Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [day] [month] [year] [list]
Date: Mon, 4 Mar 2024 16:43:09 +0100
From: Matthias Gerstner <mgerstner@...e.de>
To: oss-security@...ts.openwall.com
Subject: dnf5daemon-server: Local root Exploit and Local Denial-of-Service in
 dnf5 D-Bus Components (CVE-2024-1929, CVE-2024-1930)

Hello list,

please find below a report about a local root exploit and other issues
in dnf5daemon-server. We also offer a rendered HTML version of the
report on our blog [1].

1) Introduction
===============

The dnf5daemon-server [2] component offers a collection of D-Bus
interfaces to interact with the dnf5 package manager on the system. An
openSUSE community packager wanted to add the additional D-Bus component to
the openSUSE Tumbleweed distribution. New D-Bus system services require a
review by the SUSE security team. In the course of this review I found the
issues described in this report.

The version of dnf5 I reviewed for this is 5.1.9 [3], and any source
code references below are based on the corresponding version tag in the
upstream Git repository.

2) D-Bus Interface Design
=========================

The dnf5daemon-server offers a main interface "org.rpm.dnf.v0.SessionManager"
on which clients can create a new session, which results in a new dynamically
allocated D-Bus object being registered on the bus. This session object
provides a set of additional D-Bus interfaces for modifying package manager
configuration, for installing or removing packages or for inspecting metadata
about packages and repositories on the system.

The dnf5daemon-server is running as root and can be autostarted via the D-Bus
system bus if it is not already running. Any other users with access to the
D-Bus system bus may talk to it.

For certain privileged operations the D-Bus service implements Polkit
authentication. Only three operations are protected by Polkit:

- `org.rpm.dnf.v0.rpm.RepoConf.write`
- `org.rpm.dnf.v0.rpm.execute_transaction`
- `org.rpm.dnf.v0.rpm.Repo.confirm_key`

These relate to changing the repository configuration, executing transactions
or importing new trusted signing keys. Transactions cover all kinds of changes
introduced through the package manager.

All of these operations require `auth_admin` privileges on Polkit level. The
integration of the Polkit authorization logic is correct as far as I can tell.

The per-session D-Bus interface provided by dnf5daemon-server is rather large
and many calls take additional key/value maps to tune the behaviour of the
package manager logic contained in libdnf5. In summary, the libdnf5 library is
attached very closely to the D-Bus system bus via dnf5daemon-server.

3) Local Root Exploit via Configuration Dictionary (CVE-2024-1929)
==================================================================

While the privileged operations mentioned above are correctly protected by
Polkit, there are issues with the D-Bus interface long before Polkit is even
invoked.

The `org.rpm.dnf.v0.SessionManager.open_session` method takes a key/value map
of configuration entries. A sub-entry in this map, placed under the "config"
key, is another key/value map. The configuration values found in it will be
forwarded as configuration overrides to the `libdnf5::Base` configuration. The
spot where this happens is found in `session.cpp:63` [4].

Practically all libdnf5 configuration aspects can be influenced here, as can be
seen in the ConfigMain class in `config_main.hpp` [5].
Already when opening the session via D-Bus, the libdnf5 will be initialized
using these override configuration values. There is no sanity checking of the
content of this "config" map, which is untrusted data.

There are surely a lot of different ways to exploit this possibility to
influence the libdnf5 configuration. The simplest approach to get full root
privileges I found is to trick the library into loading a plug-in shared
library under control of the unprivileged user.

To do this, the "pluginpath" and "pluginconfpath" configuration entries need to
be supplied and need to point to a user-controlled path. There the
unprivileged user can place a configuration file that in turn points towards a
user controlled shared library. The library will then `dlopen()` this user
controlled shared library, which will lead to full code execution in the
context of the root user.

3.1) Proof of Concept
---------------------

The tarball attached to this email contains a proof of concept that I
wrote, which shows this vulnerability in action. I successfully tested
this exploit also on Fedora 39 using dnf5daemon-server version 5.1.10.
The only precondition for this exploit is that the dnf5daemon-server
package is installed on the system. Any local user, even `nobody`, can
obtain root privileges this way.

3.2) Bugfix
-----------

To fix this, I suggested to enforce a whitelist of configuration parameters
that are allowed to be overridden. Only a very small subset of values should
be allowed for unprivileged clients.

In upstream commit e51bf2f0d [6] such a whitelist enforcement has been
implemented for dnf5daemon-server.

3.3) CVE Assignment
-------------------

Red Hat Product Security assigned CVE-2024-1929 for this issue.

4) No Limit on Number of Open Sessions / Bad Session Close Behaviour (CVE-2024-1930)
====================================================================================

There is no limit on how many sessions D-Bus clients may create using the
`open_session()` D-Bus method. In my tests I was able to quickly create about
4,500 sessions and keep them open. For each session a thread is created in
dnf5daemon-server. This spends a couple of hundred megabytes of memory in the
process. Further connections will become impossible, likely because no more
threads can be spawned by the D-Bus service.

In some cases I even managed to cause the D-Bus service to `abort()` as a
result of hitting resource limits. If the service continues running and the
client disconnects, then the cleanup code found in
`SessionManager::on_name_owner_changed()` runs for each session that has been
created. Each Session holds a `ThreadsManager`, where the thread associated
with each session originates from. It is a thread that is busy-waiting to join
other threads, the code for this is found in `threads_manager.cpp:33`
[7]. Since there is a sleep of one second in this thread's loop it takes
about ~4,500 seconds for all the threads from the 4,500 sessions to be
joined one by one. The service will be unreachable for more than an
hour.

4.1) Bugfix
-----------

To fix this, I suggested to limit the number of sessions for each unprivileged
user in the system to a sensible value. Also the busy-wait loop in
`ThreadsManager` seems ill-devised. Maybe using a condition variable to inform
the cleanup thread of new work would be more efficient. Having a dedicated
`ThreadsManager` and join-thread for each session seems a bit overkill, too.

In upstream commit c090ffeb79 [8] the maximum number of sessions is
limited to three sessions. The problematic thread joining behaviour has
not been addressed as of now.

4.2) CVE Assignment
-------------------

Red Hat Product Security assigned CVE-2024-1930 for this issue.

5) Untrusted `locale` Setting for each Session
==============================================

Another part of the per-session setup is the use of an untrusted `locale`
setting in `session.cpp:54` [9]. This string is passed
to the C library's `newlocale()` function in `threads_manager.cpp:92`
[10]. A danger here is that arbitrary user controlled files or symlinks
could be processed by the C library, which could lead to various issues
like information leaks, denial of service or even code execution. I
looked into the GNU glibc implementation of `newlocale()`, and luckily
it already implements a very careful locale lookup algorithm, that
prevents that arbitrary paths are accessed if crafted `locale` strings
are passed to it. This outcome might change if a different C library is
used.

Whether anything bad could happen, apart from file system issues, when exotic
locales are selected for a thread running in dnf5daemon-server is another
question that I did not investigate more closely.

For hardening purposes I suggested that dnf5daemon-server performs a sanity
check of the `locale` string to prevent a string that contains e.g. a slash
character `/` from being used. I don't know of any upstream commits that
address this yet, but upstream stated that they intend to work on this in the
future.

6) dnfdaemon-client Demands Full Root
=====================================

Although the D-Bus service implements Polkit for privileged operations, the
dnf5daemon-client refuses to perform privileged operations for non-root users.
For example:

    user$ dnf5daemon-client distro-sync
    This command has to be run with superuser privileges (under the root user on most systems).

The code for this is found in various sub-command implementations:

    $ grep -r 'throw UnprivilegedUserError' -C 1
    dnf5daemon-client/commands/downgrade/downgrade.cpp-    if (!libdnf5::utils::am_i_root()) {
    dnf5daemon-client/commands/downgrade/downgrade.cpp:        throw UnprivilegedUserError();
    dnf5daemon-client/commands/downgrade/downgrade.cpp-    }
    --
    dnf5daemon-client/commands/distro-sync/distro-sync.cpp-    if (!libdnf5::utils::am_i_root()) {
    dnf5daemon-client/commands/distro-sync/distro-sync.cpp:        throw UnprivilegedUserError();
    dnf5daemon-client/commands/distro-sync/distro-sync.cpp-    }
    --
    dnf5daemon-client/commands/install/install.cpp-    if (!libdnf5::utils::am_i_root()) {
    dnf5daemon-client/commands/install/install.cpp:        throw UnprivilegedUserError();
    dnf5daemon-client/commands/install/install.cpp-    }
    --
    dnf5daemon-client/commands/remove/remove.cpp-    if (!libdnf5::utils::am_i_root()) {
    dnf5daemon-client/commands/remove/remove.cpp:        throw UnprivilegedUserError();
    dnf5daemon-client/commands/remove/remove.cpp-    }
    --
    dnf5daemon-client/commands/upgrade/upgrade.cpp-    if (!libdnf5::utils::am_i_root()) {
    dnf5daemon-client/commands/upgrade/upgrade.cpp:        throw UnprivilegedUserError();
    dnf5daemon-client/commands/upgrade/upgrade.cpp-    }
    --
    dnf5daemon-client/commands/reinstall/reinstall.cpp-    if (!libdnf5::utils::am_i_root()) {
    dnf5daemon-client/commands/reinstall/reinstall.cpp:        throw UnprivilegedUserError();
    dnf5daemon-client/commands/reinstall/reinstall.cpp-    }

It doesn't make sense that the client forces users to run as root (and thereby
increase the attack surface by running also the client code as root) when the
service could authenticate the user via Polkit.

I suggested to drop this root-check code in the client, and rely on the
authentication logic in the service side code. If authentication fails, then
the client code can still hint at the possibility to run the program as root.
I am not aware on any upstream commits that address this yet, but upstream
stated that they intend to work on this in the future.

7) General Review Summary
=========================

In summary my impression is that the libdnf5 library is too closely connected
to the D-Bus system bus. The library itself is unaware of the fact that it is
running with partially untrusted input. The dnf5daemon-server code needs to
carefully filter untrusted input before it is passed to the generic library
code.

8) Timeline
===========

2024-01-10: I reported the findings to secalert@...hat.com offering coordinated disclosure.
2024-01-16: We received a reply that the issue would affect Fedora only and it was suggested that I create a bug in their Bugzilla for direct communication with the dnf5 developers.
2024-01-18: I created a private Bugzilla bug [11] as suggested.
2024-01-24: The bug did not receive any attention, so I asked Red Hat Product Security once more about the procedures going forward, as the offer for coordinated disclosure has not been accepted or denied yet.
2024-01-25: We received a reply that bugfixes are being worked on, but they would likely need the full 90 days maximum embargo period for publishing updates.
2024-02-26: From the openSUSE packager for dnf5 I learned that a bugfix [6] for issue 3) was already public. So I contacted Red Hat Product Security once more to learn about the publication status of the issues.
2024-02-27: We received a reply with the two CVE assignments mentioned in this report and have been asked what our wishes are regarding a publication date.

9) References
=============

[1]: https://security.opensuse.org/2024/03/04/dnf5daemon-server-local-root.html
[2]: https://github.com/rpm-software-management/dnf5
[3]: https://github.com/rpm-software-management/dnf5/releases/tag/5.1.9 
[4]: https://github.com/rpm-software-management/dnf5/blob/5.1.9/dnf5daemon-server/session.cpp#L63
[5]: https://github.com/rpm-software-management/dnf5/blob/5.1.9/include/libdnf5/conf/config_main.hpp
[6]: https://github.com/rpm-software-management/dnf5/commit/6e51bf2f0d585ab661806076c1e428c6482ddf86
[7]: https://github.com/rpm-software-management/dnf5/blob/5.1.9/dnf5daemon-server/threads_manager.cpp#L33
[8]: https://github.com/rpm-software-management/dnf5/commit/c090ffeb79da57b88d51da6ee76f02f6512c7d91
[9]: https://github.com/rpm-software-management/dnf5/blob/5.1.9/dnf5daemon-server/session.cpp#L54
[10]: https://github.com/rpm-software-management/dnf5/blob/5.1.9/dnf5daemon-server/threads_manager.cpp#L92
[11]: https://bugzilla.redhat.com/show_bug.cgi?id=2258969

-- 
Matthias Gerstner <matthias.gerstner@...e.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

Download attachment "dnf5daemon-server.tar.gz" of type "application/gzip" (2648 bytes)

Download attachment "signature.asc" of type "application/pgp-signature" (834 bytes)

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.