This is a tool to demonstrate the possibility of obtaining (and keeping) unauthorized access to a (real or pseudo) tty device when the admin runs any command as another (evil/compromised) user. This access to the tty device can then be used to log passwords, for example.
This is meant as a demonstration tool only, please use responsibly (on your own machine, or with permission from admin).
Let's call admin the user account of the admin (i.e. the unprivileged account of the person that can use su/sudo to become root).
Let's call evil the evil/compromised user account (this account does not have any privileges).
evil@machine:~/saltty$ make
gcc -fPIC -Wall -fno-stack-protector -c -o saltty.o saltty.c
gcc -fPIC -Wall -fno-stack-protector -c -o conf.o conf.c
gcc saltty.o conf.o -lpthread -o saltty
evil@machine:~/saltty$ ./saltty
Hidden as: bash
evil@machine:~/saltty$
The daemon is now waiting in the background. The following lines are added in the daemon's log file (by default: /tmp/log.txt):
Started as PID 9351, waiting for injectable process...
Monitoring...
admin@machine:~$ su
Password:
root@machine:/home/admin# su -c "somecommand" evil
root@machine:/home/admin# exit
At this step, the daemon recovers access to the admin's tty by hijacking the "somecommand" process. The following lines are logged by the daemon:
Activity detected! Waking up.
Victim process found: 10277 (on tty: /dev/pts/5)
Injecting tty-stealer...
Injected code finished execution, restoring original process context...
Received stolen tty file descriptor from victim process!
Waiting for sniffable process...
Monitoring...
This also works with "sudo -u evil [-s] somecommand", or with any way to run any command as evil without closing stdin/stdout/stderr. An interactive shell (or even any shell) is NOT required, and "somecommand" does NOT need to have the admin's tty as a controlling terminal (an open file descriptor to it is sufficient).
admin@machine:~$ su
Password:
su: Authentication failure.
admin@machine:~$
The daemon detects an "interesting" command and log the passwords along with relevant informations:
Activity detected! Waking up.
Sniffable process found: /bin/su
[Mon Oct 3 08:38:50 2016] BIN=/bin/su CMDLINE="su" PASSWORD="blahblah"
In most cases, this tool will require adaptation to your specific use case. Various settings are available in files conf.h and conf.c.
The file conf.h has the following defines:
PID_MAX: the maximum process ID in the target system.
HIDDEN_NAME: the fake name the daemon will hide itself as. TMPDIR: any temporary directory, must be writeable/executable by evil and readable by admin.
LOG_FILE: the location of the daemon's log file. Must be in a directory writeable by evil.
INJECT_LIST: a NULL-terminated list of commands that will trigger an attempt to hijack a process to recover the admin's tty (in STEP 2). Full path to binary is required.
SNIFF_LIST: a NULL-terminated list of commands that will trigger an attempt to read a password from the admin's tty (in STEP 3). Full path to binary is required.
HANDLER_LIST: a NULL-terminated list of handlers (see next section). Each handler manages a program from SNIFF_LIST. These two lists must have the same size, as the first program is SNIFF_LIST is managed by the first handler in HANDLER_LIST, and so on.
MONITOR_MAX: this value must be greater than (or equal to) the number of elements in INJECT_LIST, HANDLER_LIST and__ SNIFF_LIST__.
READER_THREADS: in the handlers (see next section), the number of calls to the function read_tty() may not exceed READER_THREADS value.
Each time a program in SNIFF_LIST is ran from the admin's tty, the corresponding handler function (from HANDLER_LIST) is executed.
The handler's role (besides logging the password) is to display adequate prompts (such as "Password: ") and generally take care of any stuff specific to su, or sudo, or ssh, and so on.
Sample handlers exists for su and sudo, but you will need to customize them, and add more for your specific needs.
Each handler is a function taking a pointer to a state_t structure. This structure contains useful information about the program ran by the admin, and enables us to access the admin's tty.
The following useful fields exists in state_t:
int fd: this is the file descriptor to the admin's tty. Can be used for writing or ioctl/tc[s|g]etattr. DO NOT READ FROM IT. A special read_tty() function is provided for that, it is required that you use it.
int tty: a number representing the tty device (major*256 + minor).
char *name: string representing the program that triggered the password recovery attempt.
FILE *file: a "high-level" stream to the admin's tty. DO NOT READ FROM IT.
The following helper functions can be used:
ssize_t read_tty(state_t*, void*, size_t): read data from admin's tty, usable exactly like the read system call, except that you pass the state_t pointer instead of the file descriptor. May be used up to READER_THREADS time per handler invokation. Subsequent calls will always read 0 bytes.
echo_on(state_t*) / echo_off(state_t*): enable/disable admin's tty echo.
log_password(state_t*): will read a password from the admin's tty and log it. Counts as an usage of read_tty() towards the READER_THREADS limit.
Each handler can intercept up to READER_THREADS lines typed by the admin. After the handler returns, further input won't be intercepted.
A known, and somewhat related (but different) attack is the so-called TIOCSTI attack. In this attack, when root does a su evil, the user evil has the possibility of grabbing the file descriptor to the tty, and later push back ("simulate") various malicious inputs on the admin's session. This attack works only if some process running as UID evil can have the admin's tty as a controlling tty (merely having a file descriptor referring to the tty is not sufficient, as the ioctl will fail with EPERM).
It has been hypothesized that to avoid this attack, root should refrain from opening an interactive session as another user. Unfortunately, this is not sufficient. It is true that su -c "command" user correctly calls setsid() in the majority of Linux distros, denying the attacker a controlling tty (preventing the TIOCSTI attack, but not the password interception!). However, other job launch mechanisms, such as start-stop-daemon (which is AFAIK the primary mechanism to launch services in non-systemd debian-based distros) do not, unless invoked with the -b (background option).
With the -b option, start-stop_daemon performs the daemonization itself, then runs the service (in that case, there is no problem). Without -b, start-stop-daemon assumes that the actual service is responsible for for proper/secure daemonization. Unfortunately, this assumption is dangerous: even if the actual service performs the daemonization securely, and closes the tty / calls setsid() at once, there exists a window of opportunity where the process can be injected with ptrace. This race condition is trivially exploited by using inotify on the target service binary. More generally, any service launch mechanism that drops privileges before closing the tty is at risk.
The TIOCSTI attack can be tested with this tool. In conf.h, if ATTEMPT_TIOCSTI is defined, then the tool will automatically attempt a TIOCSTI attack whenever a victim process with a controlling tty can be injected. Otherwise, it will fall back to the password-interception method. The define PUSH_PAYLOAD defines the malicious command pushed back into the admin's session, and PUSH_IDLE_TIME defines the inactivity delay (in seconds) on the tty after which the program will perform the push-back. There is a pathetic attempt to hide the attack with some ANSI sequences, but it could probably be a lot better. An example of "evil" script attempting to hide from history is provided in evil.sh.
Various informations related to this attack can be found on: http://www.halfdog.net/Security/2012/TtyPushbackPrivilegeEscalation/
Example session:
evil@machine:~$ cat /tmp/evil
cp /bin/sh /tmp/blah && chmod 4755 /tmp/blah
evil@machine:~$ ./saltty
Hidden as: bash
Started as PID 5020, waiting for injectable process...
root@machine:~# start-stop-daemon --chuid evil --exec /usr/bin/myservice --start #some services are launched like this by /etc/init.d/XXX scripts in some non-systemd distros
root@machine:~# ls -l /tmp/blah
-rwsr-xr-x 1 root root 125400 oct. 4 20:30 /tmp/blah
evil@machine:~$ cat /tmp/log.txt
Monitoring...
Activity detected! Waking up.
Victim process found: 5021 (on tty: /dev/pts/4)
Victim process has a controlling terminal! Attempting ioctl(TIOCSTI) attack.
Forcing the victim process to do our bidding...
Injected code finished execution, restoring original process context...
-
ptrace() should be restricted (/proc/sys/kernel/yama/ptrace_scope) on production systems
-
When running a command as another user, the terminal (fds 0, 1, and 2) should be closed before doing setuid()
Q. How the daemon gets the tty file descriptor?
A. When the admin runs some process as the evil user, the daemon injects code into this process with ptrace(). This code sends the tty file descriptor to the daemon by unix sockets.
Q. Why the ptrace injection fails?
A. Ensure that /proc/sys/kernel/yama/ptrace_scope contains 0.
Q. Which OS/platforms are supported?
A. Only Linux/x64. Please also note that it has been only tested with GCC.
Q. How the daemon knows when a program (su, sudo, etc.) is ran?
A. By using inotify(7) on the program binaries.
Q. Why the number of tty-interceptions (READER_THREADS) is limited?
A. Because the only way (AFAIK) to get data from tty is to call read() before the legitimate password-asking program (su, sudo...). Therefore, READER_THREADS threads are created to do this, before calling the handler.
Q. How reliable is this tool?
A. There is plenty of potential issues (thread-safety problems, race conditions, etc.), but in practice it seems that it almost never causes problems. These potential issues exists because the daemon can't spend too much time checking, otherwise we may call read() too late on the admin tty (i.e. after the legitimate password-asking program).