I hope this hasn't been posted before, but I think it hasn't, it concerns a bug in ssh/sshd, allowing non-root to redirect priviliged ports on, at least, Linux, Solaris and SunOS. I've informed my ISP's sysadmin of the LocalForward problem (if you missed it, adding a line like LocalForward 80 remotehost:80 to your $HOME/.ssh/config will forward a priviliged port to a remote port, whithout needing root). Anyway, he fixed it, and I showed him the bug still works when using 2^16 + 80 (ie. 16 bit wrap). Make sure that if you decide not to remove the suid-root bit like my sysadmin, but patch ssh itself, not to make this mistake. Ok, he also fixed this problem, but then I got the idea to hack sshd using the same trick! On host1, you open an ssh connection to a machine running sshd where you have a working account using -R (RemoteForward, which is somewhat the opposite of LocalForward, but behaves the same in this case) like this: host1$ ssh -R 65621:host1.com:80 victim.com ivo's passord: victim$ (in this case, 65621 is equal to 2^16+85, i.e. port 85, the other ports were in use (by previous attempts :). And sshd on victim.com will hapilly forward priviliged port victim.com:85 to host1.com:80! Some remarks: - This could also be considered a bug in bind(), because it doesn't wrap portnumbers > 65536, but still, it makes sshd vurnerable, at least on Linux (2.0.29), Solaris 2.4 and SunOs 4.1.4 - People who patched ssh or removed the suid-bit are still vurnerable, because this is a bug in sshd, not ssh - You need to login on victim.com before sshd will redirect the port. =================================================================================== Two bugs are present, the first one does'nt check the config file for privelged ports _at_all_ (a check is done when given on the command line), the second one doesnt check for ports over 65535 which will wrap around. This problem is that ssh/sshd uses an int instead of an unsigned short to do the comparison on. So wrapping doesnt occur till its placed in the struct sockaddr_in. I've included patches (I'm not the author of ssh so these are completely unoffical), hoping anyone would point out anything I might of missed out, and ofcourse for people to use as a temporary fix till the author releases one. These are against 1.2.17 -- cut here Common subdirectories: ssh-1.2.17/gmp-2.0.2-ssh-2 and ssh-fixed-1.2.17/gmp-2.0.2-ssh-2 diff -c ssh-1.2.17/newchannels.c ssh-fixed-1.2.17/newchannels.c *** ssh-1.2.17/newchannels.c Wed Oct 30 04:27:54 1996 --- ssh-fixed-1.2.17/newchannels.c Sat Aug 23 14:19:29 1997 *************** *** 1247,1252 **** --- 1247,1256 ---- /* Check that an unprivileged user is not trying to forward a privileged port. */ + + if (port > 65535) + packet_disconnect("Requested port is %d is invalid",port); + if (port < 1024 && !is_root) packet_disconnect("Requested forwarding of port %d but user is not root.", port); diff -c ssh-1.2.17/readconf.c ssh-fixed-1.2.17/readconf.c *** ssh-1.2.17/readconf.c Wed Oct 30 04:27:53 1996 --- ssh-fixed-1.2.17/readconf.c Sat Aug 23 14:29:08 1997 *************** *** 389,394 **** --- 389,400 ---- fatal("%.200s line %d: Badly formatted port number.", filename, linenum); fwd_port = atoi(cp); + + if(fwd_port < 1024 && original_real_uid) + fatal("Port %d may only be forwarded by root.",fwd_port); + if(fwd_port > 65535) + fatal("Port %d is illegal",fwd_port); + cp = strtok(NULL, WHITESPACE); if (!cp) fatal("%.200s line %d: Missing second argument.", *************** *** 408,413 **** --- 414,425 ---- fatal("%.200s line %d: Badly formatted port number.", filename, linenum); fwd_port = atoi(cp); + + if(fwd_port < 1024 && original_real_uid) + fatal("Port %d may only be forwarded by root.",fwd_port); + if(fwd_port > 65535) + fatal("Port %d is illegal",fwd_port); + cp = strtok(NULL, WHITESPACE); if (!cp) fatal("%.200s line %d: Missing second argument.", diff -c ssh-1.2.17/ssh.c ssh-fixed-1.2.17/ssh.c *** ssh-1.2.17/ssh.c Wed Oct 30 04:27:54 1996 --- ssh-fixed-1.2.17/ssh.c Sat Aug 23 14:18:59 1997 *************** *** 483,488 **** --- 483,499 ---- usage(); /*NOTREACHED*/ } + + if(fwd_port > 65535) { + fprintf(stderr,"Illegal port specified %d\n",fwd_port); + exit(1); + } + if (fwd_port < 1024 && original_real_uid != 0) { + fprintf(stderr, + "Privileged ports can only be forwarded by root.\n"); + exit(1); + } + add_remote_forward(&options, fwd_port, buf, fwd_host_port); break; *************** *** 496,503 **** } if (fwd_port < 1024 && original_real_uid != 0) { ! fprintf(stderr, ! "Privileged ports can only be forwarded by root.\n"); exit(1); } add_local_forward(&options, fwd_port, buf, fwd_host_port); --- 507,517 ---- } if (fwd_port < 1024 && original_real_uid != 0) { ! if(fwd_port > 65535) ! fprintf(stderr,"Ilegal port specified %d\n",fwd_port); ! else ! fprintf(stderr, ! "Privileged ports can only be forwarded by root.\n"); exit(1); } add_local_forward(&options, fwd_port, buf, fwd_host_port); Common subdirectories: ssh-1.2.17/zlib-1.0.3 and ssh-fixed-1.2.17/zlib-1.0.3 -- cut here =================================================================================== We have an instance of "root process unnecessarily running under control of user". > On host1, you open an ssh connection to a machine running sshd where > you have a working account using -R (RemoteForward, which is > somewhat the opposite of LocalForward, but behaves the same in this > case) like this: > host1$ ssh -R 65621:host1.com:80 victim.com > (in this case, 65621 is equal to 2^16+85, i.e. port 85, the other ports Some weeks ago I reported as a bug that sshd runs the port forwarder as root, breaking identd queries. This exploit shows that it can lead to even nastier problems. I assume the main reason why sshd doesn't switch UIDs right after authentication is the pty chown problem. But couldn't this be done by the main sshd daemon using some sort of command channel to its children? Then the first child sshd could run as user and avoid this sort of problems. > - This could also be considered a bug in bind(), because it doesn't wrap > portnumbers > 65536, but still, it makes sshd vurnerable, at least on Linux > (2.0.29), Solaris 2.4 and SunOs 4.1.4 No. bind(2) (and connect(2)) is called with a struct sockaddr_in as argument, and this struct contains the port number as a 16-bit value sin_port somewhere. The "wrapping" happens when the assignment to this variable truncates the value to 16 bits. I think (not tried) this particular bug can be fixed like this: --- newchannels.c~ +++ newchannels.c @@ -1397,4 +1397,5 @@ /* Get arguments from the packet. */ port = packet_get_int(); + port &= 0xFFFF; hostname = packet_get_string(NULL); host_port = packet_get_int(); but the real underlying cause is that sshd does too much as root, IMHO. Wonder what more problems of this kind could come up. =================================================================================== > - This could also be considered a bug in bind(), because it doesn't wrap > portnumbers > 65536, but still, it makes sshd vurnerable, at least on Linux > (2.0.29), Solaris 2.4 and SunOs 4.1.4 Actually, the port number passed to bind() is a 16-bit quantity (the sin_port member of a struct sockaddr_in). The fix would be to compare nthos(foo.sin_port) with IPPORT_RESERVED. =================================================================================== > This problem is that ssh/sshd uses an int instead of an unsigned short to do > the comparison on. So wrapping doesnt occur till its placed in the struct > sockaddr_in. It looks like (from reading it, not from running it) the patch will consider negative port numbers to be "privileged" rather than "invalid", thus yielding the incorrect massage. This is, of course, a cosmetic problem rather than a functional one.