Hi, I've been looking at the issue for both rt 3.6 and 3.8. I have a rather full featured patch for 3.8 and I took the Debian patch for 3.6. However, I'm not happy with 3.6, it's lacking the script to fix all the passwords. I'll try to come up with something better in the next few days. Here's my WIP for reference. Regards, Xavier
diff --git a/lib/RT/User_Overlay.pm b/lib/RT/User_Overlay.pm index 1ef8c4b..459968e 100755 --- a/lib/RT/User_Overlay.pm +++ b/lib/RT/User_Overlay.pm @@ -82,6 +82,7 @@ use RT::Principals; use RT::ACE; use RT::Interface::Email; use Encode; +use Digest::SHA; # {{{ sub _Accessible @@ -1061,12 +1062,19 @@ returns an MD5 hash of the password passed in, in hexadecimal encoding. sub _GeneratePassword { my $self = shift; - my $password = shift; - - my $md5 = Digest::MD5->new(); - $md5->add(encode_utf8($password)); - return ($md5->hexdigest); - + my ($password, $salt) = @_; + + # Generate a random 4-byte salt + $salt ||= pack("C4",map{int rand(256)} 1..4); + + # Encode the salt, and a truncated SHA256 of the MD5 of the + # password The additional, un-necessary level of MD5 allows for + # transparent upgrading to this scheme, from the previous unsalted + # MD5 one. + return MIME::Base64::encode_base64( + $salt + . substr(Digest::SHA::sha256($salt . Digest::MD5::md5($password)),0,26) + ); } =head2 _GeneratePasswordBase64 PASSWORD @@ -1119,9 +1127,7 @@ sub IsPassword { my $self = shift; my $value = shift; - #TODO there isn't any apparent way to legitimately ACL this - - # RT does not allow null passwords + # RT does not allow null passwords if ( ( !defined($value) ) or ( $value eq '' ) ) { return (undef); } @@ -1136,23 +1142,32 @@ sub IsPassword { return(undef); } - # generate an md5 password - if ($self->_GeneratePassword($value) eq $self->__Value('Password')) { - return(1); - } - - # if it's a historical password we say ok. - if ($self->__Value('Password') eq crypt($value, $self->__Value('Password')) - or $self->_GeneratePasswordBase64($value) eq $self->__Value('Password')) - { - # ...but upgrade the legacy password inplace. - $self->SUPER::SetPassword( $self->_GeneratePassword($value) ); - return(1); + my $stored = $self->__Value('Password'); + if (length $stored == 40) { + # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long + my $hash = MIME::Base64::decode_base64($stored); + # The first 4 bytes are the salt, the rest is substr(SHA256,0,26) + my $salt = substr($hash, 0, 4, ""); + return substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26) eq $hash; + } elsif (length $stored == 32) { + # Hex nonsalted-md5 + return 0 unless Digest::MD5::md5_hex(Encode::encode_utf8($value)) eq $stored; + } elsif (length $stored == 22) { + # Base64 nonsalted-md5 + return 0 unless Digest::MD5::md5_base64(Encode::encode_utf8($value)) eq $stored; + } elsif (length $stored == 13) { + # crypt() output + return 0 unless crypt(Encode::encode_utf8($value), $stored) eq $stored; + } else { + $RT::Logger->warn("Unknown password form"); + return 0; } - # no password check has succeeded. get out - - return (undef); + # We got here by validating successfully, but with a legacy + # password form. Update to the most recent form. + my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self; + $obj->_Set(Field => 'Password', Value => $self->_GeneratePassword($value) ); + return 1; } # }}}
>From f0aacc11df5a13db3f55c643783753ed64d8bea0 Mon Sep 17 00:00:00 2001 From: Xavier Bachelot <xavier@xxxxxxxxxxxx> Date: Tue, 25 Jan 2011 23:25:52 +0100 Subject: [PATCH] Patch for CVE-2011-0009 --- rt3-3.8.8-salted_passwords.patch | 269 ++++++++++++++++++++++++++++++++++++++ 1 files changed, 269 insertions(+), 0 deletions(-) create mode 100644 rt3-3.8.8-salted_passwords.patch diff --git a/rt3-3.8.8-salted_passwords.patch b/rt3-3.8.8-salted_passwords.patch new file mode 100644 index 0000000..76d27f2 --- /dev/null +++ b/rt3-3.8.8-salted_passwords.patch @@ -0,0 +1,269 @@ +--- rt-3.8.8/UPGRADING ++++ rt-3.8.8/UPGRADING +@@ -18,6 +18,18 @@ + well. + + ******* ++UPGRADING FROM 3.8.8 and earlier - Changes: ++ ++Previous versions of RT used a password hashing scheme which was too ++easy to reverse, which could allow attackers with read access to the ++RT database to possibly compromise users' passwords. Even if RT does ++no password authentication itself, it may still store these weak ++password hashes -- using ExternalAuth does not guarantee that you are ++not vulnerable! To upgrade stored passwords to a stronger hash, run: ++ ++ perl etc/upgrade/vulnerable-passwords ++ ++ + UPGRADING FROM 3.8.7 and earlier - Changes: + + RT's ChartFont option has been changed from a string to a hash which +diff --git a/configure.ac b/configure.ac +index a108d56..5104d44 100755 +--- a/configure.ac ++++ b/configure.ac +@@ -397,6 +397,7 @@ AC_CONFIG_FILES([ + etc/upgrade/3.8-branded-queues-extension + etc/upgrade/3.8-ical-extension + etc/upgrade/split-out-cf-categories ++ etc/upgrade/vulnerable-passwords + sbin/rt-attributes-viewer + sbin/rt-dump-database + sbin/rt-setup-database +--- rt-3.8.8/configure ++++ rt-3.8.8/configure +@@ -3886,7 +3886,7 @@ + fi + + +-ac_config_files="$ac_config_files etc/upgrade/3.8-branded-queues-extension etc/upgrade/3.8-ical-extension etc/upgrade/split-out-cf-categories sbin/rt-attributes-viewer sbin/rt-dump-database sbin/rt-setup-database sbin/rt-test-dependencies sbin/rt-email-digest sbin/rt-email-dashboards sbin/rt-clean-sessions sbin/rt-shredder sbin/rt-validator sbin/rt-email-group-admin sbin/rt-server bin/fastcgi_server bin/mason_handler.fcgi bin/mason_handler.scgi bin/standalone_httpd bin/rt-crontool bin/rt-mailgate bin/rt" ++ac_config_files="$ac_config_files etc/upgrade/3.8-branded-queues-extension etc/upgrade/3.8-ical-extension etc/upgrade/split-out-cf-categories etc/upgrade/vulnerable-passwords sbin/rt-attributes-viewer sbin/rt-dump-database sbin/rt-setup-database sbin/rt-test-dependencies sbin/rt-email-digest sbin/rt-email-dashboards sbin/rt-clean-sessions sbin/rt-shredder sbin/rt-validator sbin/rt-email-group-admin sbin/rt-server bin/fastcgi_server bin/mason_handler.fcgi bin/mason_handler.scgi bin/standalone_httpd bin/rt-crontool bin/rt-mailgate bin/rt" + + + ac_config_files="$ac_config_files Makefile etc/RT_Config.pm lib/RT.pm bin/mason_handler.svc bin/webmux.pl t/data/configs/apache2.2+mod_perl.conf t/data/configs/apache2.2+fastcgi.conf" +@@ -4594,6 +4594,7 @@ + "etc/upgrade/3.8-branded-queues-extension") CONFIG_FILES="$CONFIG_FILES etc/upgrade/3.8-branded-queues-extension" ;; + "etc/upgrade/3.8-ical-extension") CONFIG_FILES="$CONFIG_FILES etc/upgrade/3.8-ical-extension" ;; + "etc/upgrade/split-out-cf-categories") CONFIG_FILES="$CONFIG_FILES etc/upgrade/split-out-cf-categories" ;; ++ "etc/upgrade/vulnerable-passwords") CONFIG_FILES="$CONFIG_FILES etc/upgrade/vulnerable-passwords" ;; + "sbin/rt-attributes-viewer") CONFIG_FILES="$CONFIG_FILES sbin/rt-attributes-viewer" ;; + "sbin/rt-dump-database") CONFIG_FILES="$CONFIG_FILES sbin/rt-dump-database" ;; + "sbin/rt-setup-database") CONFIG_FILES="$CONFIG_FILES sbin/rt-setup-database" ;; +@@ -5034,6 +5035,8 @@ + ;; + "etc/upgrade/split-out-cf-categories":F) chmod ug+x $ac_file + ;; ++ "etc/upgrade/vulnerable-passwords":F) chmod ug+x $ac_file ++ ;; + "sbin/rt-attributes-viewer":F) chmod ug+x $ac_file + ;; + "sbin/rt-dump-database":F) chmod ug+x $ac_file +diff --git a/etc/upgrade/vulnerable-passwords.in b/etc/upgrade/vulnerable-passwords.in +new file mode 100755 +index 0000000..c28d2b8 +--- /dev/null ++++ b/etc/upgrade/vulnerable-passwords.in +@@ -0,0 +1,93 @@ ++#!@PERL@ ++ ++use strict; ++use warnings; ++ ++use lib "@LOCAL_LIB_PATH@"; ++use lib "@RT_LIB_PATH@"; ++ ++use RT; ++RT::LoadConfig; ++RT::Init; ++ ++$| = 1; ++ ++use Getopt::Long; ++use Digest::SHA; ++my $fix; ++GetOptions("fix!" => \$fix); ++ ++use RT::Users; ++my $users = RT::Users->new( $RT::SystemUser ); ++$users->Limit( ++ FIELD => 'Password', ++ OPERATOR => 'IS NOT', ++ VALUE => 'NULL', ++ ENTRYAGGREGATOR => 'AND', ++); ++$users->Limit( ++ FIELD => 'Password', ++ OPERATOR => '!=', ++ VALUE => '*NO-PASSWORD*', ++ ENTRYAGGREGATOR => 'AND', ++); ++$users->Limit( ++ FIELD => 'Password', ++ OPERATOR => 'NOT STARTSWITH', ++ VALUE => '!', ++ ENTRYAGGREGATOR => 'AND', ++); ++push @{$users->{'restrictions'}{ "main.Password" }}, "AND", { ++ field => 'LENGTH(main.Password)', ++ op => '<', ++ value => '40', ++}; ++ ++my $count = $users->Count; ++if ($count == 0) { ++ print "No users with unsalted or weak cryptography found.\n"; ++ exit 0; ++} ++ ++if ($fix) { ++ print "Upgrading $count users...\n"; ++ while (my $u = $users->Next) { ++ my $stored = $u->__Value("Password"); ++ my $raw; ++ if (length $stored == 32) { ++ $raw = pack("H*",$stored); ++ } elsif (length $stored == 22) { ++ $raw = MIME::Base64::decode_base64($stored); ++ } elsif (length $stored == 13) { ++ printf "%20s => Old crypt() format, cannot upgrade\n", $u->Name; ++ } else { ++ printf "%20s => Unknown password format!\n", $u->Name; ++ } ++ next unless $raw; ++ ++ my $salt = pack("C4",map{int rand(256)} 1..4); ++ my $sha = Digest::SHA::sha256( ++ $salt . $raw ++ ); ++ $u->_Set( ++ Field => "Password", ++ Value => MIME::Base64::encode_base64( ++ $salt . substr($sha,0,26), ""), ++ ); ++ } ++ print "Done.\n"; ++ exit 0; ++} else { ++ if ($count < 20) { ++ print "$count users found with unsalted or weak-cryptography passwords:\n"; ++ print " Id | Name\n", "-"x9, "+", "-"x9, "\n"; ++ while (my $u = $users->Next) { ++ printf "%8d | %s\n", $u->Id, $u->Name; ++ } ++ } else { ++ print "$count users found with unsalted or weak-cryptography passwords\n"; ++ } ++ ++ print "\n", "Run again with --fix to upgrade.\n"; ++ exit 1; ++} +diff --git a/lib/RT/User_Overlay.pm b/lib/RT/User_Overlay.pm +index d64ba54..a96cbd9 100755 +--- a/lib/RT/User_Overlay.pm ++++ b/lib/RT/User_Overlay.pm +@@ -69,6 +69,7 @@ package RT::User; + use strict; + no warnings qw(redefine); + ++use Digest::SHA; + use Digest::MD5; + use RT::Principals; + use RT::ACE; +@@ -988,20 +989,28 @@ sub SetPassword { + + } + +-=head3 _GeneratePassword PASSWORD ++=head3 _GeneratePassword PASSWORD [, SALT] + +-returns an MD5 hash of the password passed in, in hexadecimal encoding. ++Returns a salted SHA-256 hash of the password passed in, in base64 ++encoding. + + =cut + + sub _GeneratePassword { + my $self = shift; +- my $password = shift; +- +- my $md5 = Digest::MD5->new(); +- $md5->add(encode_utf8($password)); +- return ($md5->hexdigest); +- ++ my ($password, $salt) = @_; ++ ++ # Generate a random 4-byte salt ++ $salt ||= pack("C4",map{int rand(256)} 1..4); ++ ++ # Encode the salt, and a truncated SHA256 of the MD5 of the ++ # password. The additional, un-necessary level of MD5 allows for ++ # transparent upgrading to this scheme, from the previous unsalted ++ # MD5 one. ++ return MIME::Base64::encode_base64( ++ $salt . substr(Digest::SHA::sha256($salt . Digest::MD5::md5($password)),0,26), ++ "" # No newline ++ ); + } + + =head3 _GeneratePasswordBase64 PASSWORD +@@ -1064,23 +1073,32 @@ sub IsPassword { + return(undef); + } + +- # generate an md5 password +- if ($self->_GeneratePassword($value) eq $self->__Value('Password')) { +- return(1); +- } +- +- # if it's a historical password we say ok. +- if ($self->__Value('Password') eq crypt(encode_utf8($value), $self->__Value('Password')) +- or $self->_GeneratePasswordBase64($value) eq $self->__Value('Password')) +- { +- # ...but upgrade the legacy password inplace. +- $self->SUPER::SetPassword( $self->_GeneratePassword($value) ); +- return(1); ++ my $stored = $self->__Value('Password'); ++ if (length $stored == 40) { ++ # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long ++ my $hash = MIME::Base64::decode_base64($stored); ++ # The first 4 bytes are the salt, the rest is substr(SHA256,0,26) ++ my $salt = substr($hash, 0, 4, ""); ++ return substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26) eq $hash; ++ } elsif (length $stored == 32) { ++ # Hex nonsalted-md5 ++ return 0 unless Digest::MD5::md5_hex(encode_utf8($value)) eq $stored; ++ } elsif (length $stored == 22) { ++ # Base64 nonsalted-md5 ++ return 0 unless Digest::MD5::md5_base64(encode_utf8($value)) eq $stored; ++ } elsif (length $stored == 13) { ++ # crypt() output ++ return 0 unless crypt(encode_utf8($value), $stored) eq $stored; ++ } else { ++ $RT::Logger->warn("Unknown password form"); ++ return 0; + } + +- # no password check has succeeded. get out +- +- return (undef); ++ # We got here by validating successfully, but with a legacy ++ # password form. Update to the most recent form. ++ my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self; ++ $obj->_Set(Field => 'Password', Value => $self->_GeneratePassword($value) ); ++ return 1; + } + + sub CurrentUserRequireToSetPassword { +diff --git a/sbin/rt-test-dependencies.in b/sbin/rt-test-dependencies.in +index aaebb0d..a8513fb 100755 +--- a/sbin/rt-test-dependencies.in ++++ b/sbin/rt-test-dependencies.in +@@ -208,6 +208,7 @@ sub text_to_hash { + $deps{'CORE'} = [ text_to_hash( << '.') ]; + Digest::base + Digest::MD5 2.27 ++Digest::SHA + DBI 1.37 + Class::ReturnValue 0.40 + DBIx::SearchBuilder 1.54 -- 1.7.3.5
--- /home/xavierb/rpm/SPECS/rt3.spec 2009-12-04 22:33:31.000000000 +0100 +++ /home/xavierb/rpm/SPECS/rt3.spec.modified 2011-01-25 15:57:19.000000000 +0100 @@ -13,7 +13,7 @@ Name: rt3 Version: 3.6.10 -Release: 1%{?dist} +Release: 2%{?dist} Summary: Request tracker 3 Group: Applications/Internet @@ -27,6 +27,7 @@ Patch0: rt-3.6.1-config.diff Patch1: rt-3.4.1-I18N.diff Patch2: rt-3.6.0-Makefile.diff +Patch3: rt3-3.6.10-salted_passwords.patch BuildArch: noarch BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) @@ -102,6 +103,7 @@ %patch0 -p1 %patch1 -p1 %patch2 -p1 +%patch3 -p1 # Patch backups added by rpm disturb find -name '*.orig' -exec rm -f {} \; @@ -280,6 +282,9 @@ %{_mandir}/man1/rt-mailgate* %changelog +* Tue Jan 25 2011 Xavier Bachelot <xavier@xxxxxxxxxxxx> - 3.6.10-2 +- Add Debian patch for CVE-2011-0009. + * Thu Dec 03 2009 Xavier Bachelot <xavier@xxxxxxxxxxxx> - 3.6.10-1 - Update to 3.6.10 for CVE-2009-3585 : session hijack.
-- Fedora Extras Perl SIG http://www.fedoraproject.org/wiki/Extras/SIGs/Perl perl-devel mailing list perl-devel@xxxxxxxxxxxxxxxxxxxxxxx https://admin.fedoraproject.org/mailman/listinfo/perl-devel