From a56ce1ee29f2f69cd28b2341d0399794777bda53 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Thu, 27 Jun 2024 14:58:15 -0300 Subject: [PATCH] Table `users` on linux by default to return only users in `/etc/passwd` (#8342) #8337 For manual testing I used [this](https://www.analogous.dev/blog/sssd-without-tls/) guide to setup a local OpenLDAP directory server and a Ubuntu VM that uses such server for authentication. The Ubuntu VM has 51 local users and 2 "remote" users (joe,uid:1005 and julie,uid:1006) in an LDAP directory. ```sqlite SELECT uid,username FROM users; // returns 51 local users as expected +-------+---------------------+ | uid | username | +-------+---------------------+ | 0 | root | | 1 | daemon | | 2 | bin | | 3 | sys | | 4 | sync | | 5 | games | | 6 | man | | 7 | lp | | 8 | mail | | 9 | news | | 10 | uucp | | 13 | proxy | | 33 | www-data | | 34 | backup | | 38 | list | | 39 | irc | | 41 | gnats | | 65534 | nobody | | 100 | systemd-network | | 101 | systemd-resolve | | 102 | messagebus | | 103 | systemd-timesync | | 104 | syslog | | 105 | _apt | | 106 | tss | | 107 | uuidd | | 108 | systemd-oom | | 109 | tcpdump | | 110 | avahi-autoipd | | 111 | usbmux | | 112 | dnsmasq | | 113 | kernoops | | 114 | avahi | | 115 | cups-pk-helper | | 116 | rtkit | | 117 | whoopsie | | 118 | sssd | | 119 | speech-dispatcher | | 120 | fwupd-refresh | | 121 | nm-openvpn | | 122 | saned | | 123 | colord | | 124 | geoclue | | 125 | pulse | | 126 | gnome-initial-setup | | 127 | hplip | | 128 | gdm | | 1000 | luk | | 1002 | citrixlog | | 129 | openldap | | 1003 | zoo | +-------+---------------------+ SELECT uid,username FROM users WHERE include_remote=1; // returns 53 users as expected +-------+---------------------+ | uid | username | +-------+---------------------+ | 0 | root | | 1 | daemon | | 2 | bin | | 3 | sys | | 4 | sync | | 5 | games | | 6 | man | | 7 | lp | | 8 | mail | | 9 | news | | 10 | uucp | | 13 | proxy | | 33 | www-data | | 34 | backup | | 38 | list | | 39 | irc | | 41 | gnats | | 65534 | nobody | | 100 | systemd-network | | 101 | systemd-resolve | | 102 | messagebus | | 103 | systemd-timesync | | 104 | syslog | | 105 | _apt | | 106 | tss | | 107 | uuidd | | 108 | systemd-oom | | 109 | tcpdump | | 110 | avahi-autoipd | | 111 | usbmux | | 112 | dnsmasq | | 113 | kernoops | | 114 | avahi | | 115 | cups-pk-helper | | 116 | rtkit | | 117 | whoopsie | | 118 | sssd | | 119 | speech-dispatcher | | 120 | fwupd-refresh | | 121 | nm-openvpn | | 122 | saned | | 123 | colord | | 124 | geoclue | | 125 | pulse | | 126 | gnome-initial-setup | | 127 | hplip | | 128 | gdm | | 1000 | luk | | 1002 | citrixlog | | 129 | openldap | | 1003 | zoo | | 1005 | joe | | 1006 | julie | +-------+---------------------+ SELECT * FROM users where uid = 1000; // returns a local user luk as expected +------+------+------------+------------+----------+-------------+-----------+-----------+------+ | uid | gid | uid_signed | gid_signed | username | description | directory | shell | uuid | +------+------+------------+------------+----------+-------------+-----------+-----------+------+ | 1000 | 1000 | 1000 | 1000 | luk | Lucas,,, | /home/luk | /bin/bash | | +------+------+------------+------------+----------+-------------+-----------+-----------+------+ SELECT * FROM users where username = 'luk'; // returns a local user luk as expected +------+------+------------+------------+----------+-------------+-----------+-----------+------+ | uid | gid | uid_signed | gid_signed | username | description | directory | shell | uuid | +------+------+------------+------------+----------+-------------+-----------+-----------+------+ | 1000 | 1000 | 1000 | 1000 | luk | Lucas,,, | /home/luk | /bin/bash | | +------+------+------------+------------+----------+-------------+-----------+-----------+------+ SELECT * FROM users where username = 'luk' OR uid < 10; // returns a local user luk + other local users as expected +------+-------+------------+------------+----------+-------------+-----------------+-------------------+------+ | uid | gid | uid_signed | gid_signed | username | description | directory | shell | uuid | +------+-------+------------+------------+----------+-------------+-----------------+-------------------+------+ | 1000 | 1000 | 1000 | 1000 | luk | Lucas,,, | /home/luk | /bin/bash | | | 0 | 0 | 0 | 0 | root | root | /root | /bin/bash | | | 1 | 1 | 1 | 1 | daemon | daemon | /usr/sbin | /usr/sbin/nologin | | | 2 | 2 | 2 | 2 | bin | bin | /bin | /usr/sbin/nologin | | | 3 | 3 | 3 | 3 | sys | sys | /dev | /usr/sbin/nologin | | | 4 | 65534 | 4 | 65534 | sync | sync | /bin | /bin/sync | | | 5 | 60 | 5 | 60 | games | games | /usr/games | /usr/sbin/nologin | | | 6 | 12 | 6 | 12 | man | man | /var/cache/man | /usr/sbin/nologin | | | 7 | 7 | 7 | 7 | lp | lp | /var/spool/lpd | /usr/sbin/nologin | | | 8 | 8 | 8 | 8 | mail | mail | /var/mail | /usr/sbin/nologin | | | 9 | 9 | 9 | 9 | news | news | /var/spool/news | /usr/sbin/nologin | | +------+-------+------------+------------+----------+-------------+-----------------+-------------------+------+ SELECT * FROM users where (username = 'luk' OR uid < 10) AND include_remote=1; // returns a local user luk + other local users as expected +------+-------+------------+------------+----------+-------------+-----------------+-------------------+------+ | uid | gid | uid_signed | gid_signed | username | description | directory | shell | uuid | +------+-------+------------+------------+----------+-------------+-----------------+-------------------+------+ | 0 | 0 | 0 | 0 | root | root | /root | /bin/bash | | | 1 | 1 | 1 | 1 | daemon | daemon | /usr/sbin | /usr/sbin/nologin | | | 2 | 2 | 2 | 2 | bin | bin | /bin | /usr/sbin/nologin | | | 3 | 3 | 3 | 3 | sys | sys | /dev | /usr/sbin/nologin | | | 4 | 65534 | 4 | 65534 | sync | sync | /bin | /bin/sync | | | 5 | 60 | 5 | 60 | games | games | /usr/games | /usr/sbin/nologin | | | 6 | 12 | 6 | 12 | man | man | /var/cache/man | /usr/sbin/nologin | | | 7 | 7 | 7 | 7 | lp | lp | /var/spool/lpd | /usr/sbin/nologin | | | 8 | 8 | 8 | 8 | mail | mail | /var/mail | /usr/sbin/nologin | | | 9 | 9 | 9 | 9 | news | news | /var/spool/news | /usr/sbin/nologin | | | 1000 | 1000 | 1000 | 1000 | luk | Lucas,,, | /home/luk | /bin/bash | | +------+-------+------------+------------+----------+-------------+-----------------+-------------------+------+ SELECT * FROM users where (username = 'julie' OR uid = 1005) AND include_remote=1; // returns the two remote users as expected +------+-----+------------+------------+----------+-------------+-------------+---------+------+ | uid | gid | uid_signed | gid_signed | username | description | directory | shell | uuid | +------+-----+------------+------------+----------+-------------+-------------+---------+------+ | 1005 | 600 | 1005 | 600 | joe | joe | /home/joe | /bin/sh | | | 1006 | 600 | 1006 | 600 | julie | julie | /home/julie | /bin/sh | | +------+-----+------------+------------+----------+-------------+-------------+---------+------+ SELECT * FROM users where (username = 'julie' OR uid = 1005); // returns empty as expected ``` --- osquery/tables/system/linux/users.cpp | 91 +++++++++++++++++++++++++-- specs/users.table | 3 + tests/integration/tables/users.cpp | 45 +++++++++++-- 3 files changed, 130 insertions(+), 9 deletions(-) diff --git a/osquery/tables/system/linux/users.cpp b/osquery/tables/system/linux/users.cpp index 7d00139d361..6134db22453 100644 --- a/osquery/tables/system/linux/users.cpp +++ b/osquery/tables/system/linux/users.cpp @@ -7,6 +7,7 @@ * SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only) */ +#include #include #include @@ -18,7 +19,9 @@ namespace osquery { namespace tables { -void genUser(const struct passwd* pwd, QueryData& results) { +void genUser(const struct passwd* pwd, + QueryData& results, + const std::string& include_remote) { Row r; r["uid"] = BIGINT(pwd->pw_uid); r["gid"] = BIGINT(pwd->pw_gid); @@ -41,10 +44,11 @@ void genUser(const struct passwd* pwd, QueryData& results) { r["shell"] = SQL_TEXT(pwd->pw_shell); } r["pid_with_namespace"] = "0"; + r["include_remote"] = include_remote; results.push_back(r); } -QueryData genUsersImpl(QueryContext& context, Logger& logger) { +QueryData genUsersImplIncludeRemote(QueryContext& context, Logger& logger) { QueryData results; struct passwd pwd; struct passwd* pwd_results{nullptr}; @@ -62,7 +66,7 @@ QueryData genUsersImpl(QueryContext& context, Logger& logger) { if (auid_exp.isValue()) { getpwuid_r(auid_exp.get(), &pwd, buf.get(), bufsize, &pwd_results); if (pwd_results != nullptr) { - genUser(pwd_results, results); + genUser(pwd_results, results, "1"); } } } @@ -71,7 +75,7 @@ QueryData genUsersImpl(QueryContext& context, Logger& logger) { for (const auto& username : usernames) { getpwnam_r(username.c_str(), &pwd, buf.get(), bufsize, &pwd_results); if (pwd_results != nullptr) { - genUser(pwd_results, results); + genUser(pwd_results, results, "1"); } } } else { @@ -81,7 +85,7 @@ QueryData genUsersImpl(QueryContext& context, Logger& logger) { if (pwd_results == nullptr) { break; } - genUser(pwd_results, results); + genUser(pwd_results, results, "1"); } endpwent(); } @@ -89,6 +93,82 @@ QueryData genUsersImpl(QueryContext& context, Logger& logger) { return results; } +QueryData genUsersImplLocal(QueryContext& context, Logger& logger) { + // + // Either "username" or "uid" is set on the constraints, not both. + // + const auto usernames = context.constraints["username"].getAll(EQUALS); + const auto uids = [&context]() -> std::set { + std::set uids; + const auto uid_constraints = context.constraints["uid"].getAll(EQUALS); + for (const auto& uid_constraint : uid_constraints) { + auto const auid_exp = tryTo(uid_constraint, 10); + if (auid_exp.isValue()) { + uids.insert(auid_exp.get()); + } + } + return uids; + }(); + + // + // We are avoiding the use of setpwent, getpwent_r, endpwent, getpwnam_r and + // getpwuid_r to prevent osquery sending requests to LDAP directories on + // hosts that have LDAP configured for authentication. + // (See https://github.com/osquery/osquery/issues/8337.) + // + QueryData results; + FILE* passwd_file = fopen("/etc/passwd", "r"); + if (passwd_file == nullptr) { + LOG(ERROR) << "could not open /etc/passwd file: " << std::strerror(errno); + return results; + } + + size_t bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); + if (bufsize > 16384) { // value was indeterminate + bufsize = 16384; // should be more than enough + } + auto buf = std::make_unique(bufsize); + + struct passwd pwd; + struct passwd* result{nullptr}; + int ret; + while (1) { + ret = fgetpwent_r(passwd_file, &pwd, buf.get(), bufsize, &result); + if (ret != 0 || result == nullptr) { + break; + } + if (!usernames.empty()) { + if (usernames.find(result->pw_name) == usernames.end()) { + continue; + } + } else if (!uids.empty()) { + if (uids.find(result->pw_uid) == uids.end()) { + continue; + } + } + genUser(result, results, "0"); + } + + if (ret != 0 && ret != ENOENT) { + LOG(ERROR) << "failed to iterate /etc/passwd file: " + << std::strerror(errno); + } + fclose(passwd_file); + + return results; +} + +QueryData genUsersImpl(QueryContext& context, Logger& logger) { + auto include_remote = 0; + if (context.hasConstraint("include_remote", EQUALS)) { + include_remote = context.constraints["include_remote"].matches(1); + } + if (include_remote) { + return genUsersImplIncludeRemote(context, logger); + } + return genUsersImplLocal(context, logger); +} + QueryData genUsers(QueryContext& context) { if (hasNamespaceConstraint(context)) { return generateInNamespace(context, "users", genUsersImpl); @@ -97,5 +177,6 @@ QueryData genUsers(QueryContext& context) { return genUsersImpl(context, logger); } } + } // namespace tables } // namespace osquery diff --git a/specs/users.table b/specs/users.table index e0c7258817f..781952ae648 100644 --- a/specs/users.table +++ b/specs/users.table @@ -20,6 +20,9 @@ extended_schema(DARWIN, [ extended_schema(LINUX, [ Column("pid_with_namespace", INTEGER, "Pids that contain a namespace", additional=True, hidden=True), ]) +extended_schema(LINUX, [ + Column("include_remote", INTEGER, "1 to include remote (LDAP/AD) accounts (default 0). Warning: without any uid/username filtering it may list whole LDAP directories", additional=True, hidden=True), +]) implementation("users@genUsers") examples([ "select * from users where uid = 1000", diff --git a/tests/integration/tables/users.cpp b/tests/integration/tables/users.cpp index 27802025088..e035694fd06 100644 --- a/tests/integration/tables/users.cpp +++ b/tests/integration/tables/users.cpp @@ -70,17 +70,54 @@ TEST_F(UsersTest, test_sanity) { row_map.emplace("uuid", NormalType); } + // + // The returned user might be "root" or a test user created as + // part of the Github CI action (see .github/workflows/hosted_runners.yml + // and .github/workflows/self_hosted_runners.yml). + // + // select * case auto const rows = execute_query("select * from users"); ASSERT_GE(rows.size(), 1ul); validate_rows(rows, row_map); - // select with a specified uid auto test_uid = rows.front().at("uid"); - auto const rows_one = + auto test_username = rows.front().at("username"); + + // select with a specified uid + auto const rows_uid = execute_query(std::string("select * from users where uid=") + test_uid); - ASSERT_GE(rows_one.size(), 1ul); - validate_rows(rows_one, row_map); + ASSERT_GE(rows_uid.size(), 1ul); + validate_rows(rows_uid, row_map); + + // select with a specified username + auto const rows_username = + execute_query(std::string("select * from users where username='") + + test_username + "'"); + ASSERT_GE(rows_username.size(), 1ul); + validate_rows(rows_username, row_map); + +#ifdef OSQUERY_LINUX + // select with include_remote flag set + auto const rows_include_remote = + execute_query(std::string("select * from users where include_remote=1")); + ASSERT_GE(rows_include_remote.size(), 1ul); + validate_rows(rows_include_remote, row_map); + + // select with a specified uid and include_remote flag set + auto const rows_uid_include_remote = execute_query( + std::string("select * from users where include_remote=1 and uid=") + + test_uid); + ASSERT_GE(rows_uid_include_remote.size(), 1ul); + validate_rows(rows_uid_include_remote, row_map); + + // select with a specified username and include_remote flag set + auto const rows_username_include_remote = execute_query( + std::string("select * from users where include_remote=1 and username='") + + test_username + "'"); + ASSERT_GE(rows_username_include_remote.size(), 1ul); + validate_rows(rows_username_include_remote, row_map); +#endif } } // namespace table_tests