Perl5におけるコンテキストの扱いによる脆弱性について

The Perl Jam - Exploiting a 20 Year-old Vulnerability より

Perlを普段から書いている人にとっては常識ではあるが、そうではない人のために書いておく。

リストと配列

Perlにおける「リスト」とは何かというのを確認しておく。まず、Perlにはコンテキストという他のプログラミング言語にはない概念があり、単数(スカラー)と複数(リスト)を区別する。

# 配列
my @a = ('a', 'b', 'c');
# リストコンテキスト
# 配列をリストコンテキストで評価
print @a; #=> abc
# リストをリストコンテキストで評価
print ('a', 'b', 'c'); #=> abc

# スカラーコンテキスト
# 配列の要素はスカラー
print $a[0]; #=> a
# 配列をスカラーコンテキストで評価
print scalar @a; #=> 3
# リストをスカラーコンテキストで評価
print scalar ('a', 'b', 'c'); #=> c
# Useless use of a constant ("a") in void context
# Useless use of a constant ("b") in void context

コンテキストによって返される値が違うことが分かる。

リストと配列は別物。リストを@aという変数に入れることで配列になる。*1
ただし、配列はリストコンテキストで評価される。

sub foo {
    my ($arg1, $arg2, $arg3) = @_;
    # $arg1, $arg2, $arg3 にはどのような値が代入されるか
}

foo(1, 2, 3); #=> (1, 2, 3)
my @a = (1, 2);
foo(@a, 3);   #=> (1, 2, 3)

my @b = (2, 3);
foo(1, $b[0], 3); #=> (1, 2, 3)
foo(1, @b[0, 1]); #=> (1, 2, 3)
foo(1, @b);       #=> (1, 2, 3)
# array reference
foo(1, \@b);      #=> (1, [2, 3])
wantarray

wantarrayを使うと、コンテキスト(単数を受け取る場面、複数を受け取る場面)によって関数の振る舞いを変えることができる。しかし、これを使うとコードを見ただけでは動作が分からなくなってしまうこともあるため、頻繁には使われていない。使われる例を示すとすれば、CGI.pmのparam関数がある。

use CGI;
my $cgi = CGI->new;
# a=1&a=2&a=3
my @values = $cgi->param('a'); # (1, 2, 3)
my $value = $cgi->param('a'); # 1

ただし、CGI.pm 4.08からはmulti_paramを使うことがが推奨されており、リストコンテキストでparamを使おうとすると警告が出る。

my @values = $cgi->multi_param('a'); # (1, 2, 3)

外部からのパラメータを引数として渡すとき、この仕様が大きく問題となる。明示的にscalarによってスカラーコンテキストで評価しない限り、リストコンテキストで評価される。
New Class of Vulnerability in Perl Web Applications | Hacking for Christ では以下のようなコードに脆弱性があると言及されている。

my $otheruser = Bugzilla::User->create({
    login_name => $login_name, 
    realname   => $cgi->param('realname'), 
    cryptpassword => $password});

$cgi->param('realname')はリストコンテキストで評価されるため、realnameパラメータが複数指定されていた場合に、引数の改竄ができる。
realname=aaa&realname=login_name&realname=adminとrealnameに対して複数のパラメータを指定することで以下のようにlogin_nameの改竄ができる。

{
    login_name => $login_name, 
    realname   => 'aaa', 
    login_name => 'admin',
    cryptpassword => $password,
}

このように書くのが正しい。

my $otheruser = Bugzilla::User->create({
    login_name => $login_name, 
    realname   => scalar $cgi->param('realname'), 
    cryptpassword => $password});

もしくは

my $realname = $cgi->param('realname');
my $otheruser = Bugzilla::User->create({
    login_name => $login_name, 
    realname   => $realname, 
    cryptpassword => $password});

この問題は脆弱なアプリを書く技術 // Speaker Deckでも言及しており、Advent Calendar CTF 2014の13日目の問題として出題した。Perlのハッシュはkey/valueが対になっていなくても(要素数が奇数)、警告は出るが動くため上記の例であればcryptpasswordも書き換えることができる。詳しくはスライド参照。

余談ではあるが、Mojolicious 5.48からはMojo::Parametersのparamの振る舞いが変更され、以前はCGI.pmと同じ挙動をしていたが、wantarrayによるコンテキスト別の挙動は完全に廃止され、複数のパラメータを受け取るにはevery_paramを使うようになった。

蛇足

できるだけ安全にコンテキストを扱うにはscalarスカラーコンテキストで評価するのを明示的に書く必要がある。しかし、scalarと毎回タイプするには人生は短すぎる。
Perl5では単項演算子としての+は意味のないものとして扱う。code blockとhash referenceを区別するものとして使われることもある。Perl6では+単項演算がscalarとして扱われるようになった。
ここではPerl5の+がscalarの代わりになるように言語ハックをしてみる。blead-perlに以下のパッチを当てる。

diff --git a/perly.y b/perly.y
index 4b73977..ec0cdf6 100644
--- a/perly.y
+++ b/perly.y
@@ -824,7 +824,13 @@ termbinop: term ASSIGNOP term                     /* $x = $y */
 termunop : '-' term %prec UMINUS                       /* -$x */
                        { $$ = newUNOP(OP_NEGATE, 0, scalar($2)); }
        |       '+' term %prec UMINUS                  /* +$x */
-                       { $$ = $2; }
+                       {
+                         if (FEATURE_UNARYSCALAR_IS_ENABLED) {
+                           $$ = scalar($2);
+                         } else {
+                           $$ = $2;
+                         }
+                       }

        |       '!' term                               /* !$x */
                        { $$ = newUNOP(OP_NOT, 0, scalar($2)); }
diff --git a/regen/feature.pl b/regen/feature.pl
index 6733e3c..c784691 100755
--- a/regen/feature.pl
+++ b/regen/feature.pl
@@ -37,6 +37,7 @@ my %feature = (
     unicode_strings => 'unicode',
     fc              => 'fc',
     signatures      => 'signatures',
+    unaryscalar     => 'unaryscalar',
 );

 # NOTE: If a feature is ever enabled in a non-contiguous range of Perl
% perl regen/feature.pl
% perl regen_perly.pl
% perl -MPerl::Build -e'Perl::Build->install(src_path => ".", dst_path => "/opt/blead-perl", configure_options => ["-Dusedevel", "-de"])'
% bin/perl5.21.10 -e'print localtime()'
657152711155570
% bin/perl5.21.10 -e'print scalar localtime()'
Fri Feb 27 15:57:09 2015
% bin/perl5.21.10 -Mfeature=unaryscalar -e'print +localtime()'
Fri Feb 27 15:57:11 2015

こうして世界がまたひとつ平和に近づいたとさ ;)