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 未继续执行下去,把带攻击的字符串给原样返回了。