Perl 安全
Table of Contents
1. 列表问题
先看一个例子:
use CGI; my $cgi = CGI->new(); sub update_user_password { return ( "username" => $cgi->param("username"), "password" => "123" ); } %user_info = update_user_password(); while (($k, $v) = each %user_info) { print "$k: $v\n"; }
正常请求 /user.pl?username=test 时,返回结果:
username: test password: 123
但是,当传入 URL 参数的 key 重复多次时:/user.pl?username=test&username=username&username=admin
返回结果:
password: 123 username: admin
HTTP 参数受到了污染。
进一步了解 Perl 的 hash 和 list,再看一个例子:
@list = ("member", "user", "admin"); %hash = ( "user" => "test", "password" => "123", "level" => @list ); while (($k, $v) = each %hash) { print "$k: $v\n"; }
执行结果:
level: member password: 123 user: admin
总结一句:Perl 的 hash 中如果引入了 list,那么 list 将会按键对值的结构扁平展开到 hash 中。
进一步分析,当 URL 传入多个同名参数时,$cgi->param 子程序返回的是一个列表。上例中 username=test&username=username&username=admin 返回的是("test", "username", "admin"),这时就和 hash 结构扁平合并,第一个元素 test 则设置成 level 键的指;剩下的 username 和 admin 则单独组成为一对键值,但由于原本的 hash 存在 username 键,所以对应的值被覆盖了。
真实案例——CVE-2014-1572(Bugzilla 越权漏洞)
存在漏洞的代码如下:
my $otheruser = Bugzilla::User->create({ login_name => $login_name, realname => $cgi->param('realname'), cryptpassword => $password});
当提交下面请求内容时:
a=confirm_new_account&t=[TOKEN]&passwd1=[password]&passwd2=[password] &realname=test&realname=login_name&[email protected]
传递给 User->create 子程序的结构如下:
{ realname => 'test', login_name => '[email protected]', cryptpassword => $password }
因为 realname=test&realname=loginname&[email protected] 这句导致 loginname 被覆盖了。
2. 参数传递引发的安全问题
看下面这段子程序:
sub hello { ($a, $b, $c) = @_; print "$a$b$c\n"; }
观察调用结果:
hello(1, 2, 3); # => 123 hello((1, 2, 3)); # => 123 hello(1, (2, 3)); # => 123 hello(1, (2, 3), 4) # => 123
仔细观察规律,我们可以看到,当传递给子程序的参数即便不够,传递的数组也会被平坦展开并赋值给 $a、$b、$c 三个变量上;最后一个调用的最后一个参数“4”,并没有按我们所想象的,把它赋值给 $c。
这种可以轻易覆盖参数的特性会导致什么问题呢,我们看看 DBI 库中有个 quote 子程序,它原本可以用来防止 SQL 注入的,完整实现代码如下:
# file: lib/DBI/DBD/SqlEngine.pm sub quote ($$;$) { my ( $self, $str, $type ) = @_; defined $str or return "NULL"; defined $type && ( $type == DBI::SQL_NUMERIC() || $type == DBI::SQL_DECIMAL() || $type == DBI::SQL_INTEGER() || $type == DBI::SQL_SMALLINT() || $type == DBI::SQL_FLOAT() || $type == DBI::SQL_REAL() || $type == DBI::SQL_DOUBLE() || $type == DBI::SQL_TINYINT() ) and return $str; $str =~ s/\\/\\\\/sg; $str =~ s/\0/\\0/sg; $str =~ s/\'/\\\'/sg; $str =~ s/\n/\\n/sg; $str =~ s/\r/\\r/sg; return "'$str'"; }
quote 原本要接受三个参数的,其中第二个参数 $str 就是要过滤的字符串。只要 $type 参数符合 DBI::SQLNUMERIC() 等结果,就不经过滤原样返回字符串,现在做的就是覆盖掉 $type 参数。
先看一段 CGI 脚本代码:
use CGI; use DBI; print "Content-Type: text/html\n\n"; $cgi = CGI->new(); $dsn = "DBI:mysql:mysql:localhost:3306"; my $db = DBI->connect($dsn, "root", "root") or die $DBI::errstr; $user = $db->quote($cgi->param('user')); print $user;
如果参数中带危险字符:/index.pl?user=admin">',页面返回被过滤的字符串:
'admin\">\''
现在来覆盖参数,访问:/index.pl?user=admin">'&user=2,页面返回:
admin">'2
如上所示,quote 未转义字符串,因为最终传递给 quote 子程序的参数是一个数组——把 $type 变量给覆盖成”2“了,而 DBI::SQLNUMERIC() 的返回值也是 2,让 quote 未继续执行下去,把带攻击的字符串给原样返回了。