てきとうなメモ

本の感想とか技術メモとか

Apache Commons FileUploadの脆弱性(CVE-2016-3092)

JVNDB-2016-000121 - Apache Commons FileUpload におけるサービス運用妨害 (DoS) の脆弱性

Apache Commons FileUploadにDoS攻撃脆弱性なのだが、攻撃方法の詳細は説明されていない。調べたところ、推測なのだが、2年前のDoS攻撃脆弱性(CVE-2014-0050)と関係するものではないかと思う。

2年前の脆弱性はこちら。

JVN#14876762: Apache Commons FileUpload におけるサービス運用妨害 (DoS) の脆弱性

この脆弱性はboundaryに長い文字列を指定してマルチパートのPOSTリクエストを送ると発生し、無限ループに陥ることがあるというものである。

この脆弱性に対するPoCはネットを探せば見つかる。どうも以下の2つの条件を満たすと攻撃できるようだ

  • boundaryを4092バイト以上にする
  • bodyに4097バイト以上のboundaryを含まないデータを入れる

適当にサーブレットを作ってPoCを実行すると無限ループに陥る。サーブレットを停止してスタックトレースを見ると以下でループしているようだ。
commons uploadのバージョンは1.3を用いた

MultipartStream$ItemInputStream.makeAvailable() line: 1016    
MultipartStream$ItemInputStream.read(byte[], int, int) line: 901    
MultipartStream$ItemInputStream(InputStream).read(byte[]) line: 101    
Streams.copy(InputStream, OutputStream, boolean, byte[]) line: 101    
Streams.copy(InputStream, OutputStream, boolean) line: 70    
MultipartStream.readBodyData(OutputStream) line: 589    
MultipartStream.discardBodyData() line: 613    
MultipartStream.skipPreamble() line: 630    
FileUploadBase$FileItemIteratorImpl.findNextItem() line: 1018   
FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase, RequestContext) line: 998    
DiskFileUpload(FileUploadBase).getItemIterator(RequestContext) line: 310    
DiskFileUpload(FileUploadBase).parseRequest(RequestContext) line: 334   
DiskFileUpload(FileUploadBase).parseRequest(HttpServletRequest) line: 288 

ループしているのはMultipartStream$ItemInputStream.makeAvailableだが、このメソッドを説明するためには各クラスの説明を先にした方が良い。
関係する主要なクラスはMultipartStreamとItemInputStreamである。

MultipartStreamはマルチパートのPOSTリクエストをparseするクラスであり、子クラスItemInputStreamはboundaryで分割された現在のitemを読みだすためのクラスである。

この2つのクラスは効率化のために内部のバッファを利用している。このバッファの処理が脆弱性の原因である。

まず、MultipartStreamはバッファ関係のフィールドとして以下のものを持っている。

byte[] buffer バッファを入れるバイト配列
int bufSize bufferのサイズ
int head buffer上の実際のバッファの開始インデックス
int tail buffer上の実際のバッファの終了インデックス
byte[] boundary boundary+prefixを入れておくバイト配列
int boundaryLength boundary.length
int keepRegion boundaryをバッファから探すために必要なサイズ

さらに、ItemInputStream側にもバッファ関係のフィールドとして以下のものがある。

int pos boundaryを検索した時の、boundaryのMultipartStream.buffer上の開始インデックス
int pad boundaryを検索するために保持すべき実際のバイト数。MultipartStream.keepRegionと似たようなもの

headとtailはbuffer内の有効な部分を指している。1バイト読みだすとheadをインクリメントする。head=tailになるとInputStreamから実際に読み出してbufferに入れている。

boundaryはリクエストで指定されたboundaryを入れるバイト列だが、それだけでなくprefixに[CR,LF,-,-]がくっついている。これは実際の区切り文字にはboundaryに--がくっつくためであり、さらに行頭なのでCR,LFがprefixとしてくっついている。この辺りはコンストラクタのコードを見ることで確認できる。

MultipartStream(InputStream input,
      byte[] boundary,
      int bufSize,
      ProgressNotifier pNotifier) {
  ...
  // BOUNDARY_PREFIXは[CR, LF, -, -]
  this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length];
  this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
  this.keepRegion = this.boundary.length;
  System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
          BOUNDARY_PREFIX.length);
  System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
          boundary.length);
  ...
}

MultipartStream内では、buffer内からboundaryを検索する際にboundaryの途中までマッチしたという情報を残していない。そのままだと読み込んだ単位の境にboundaryがある場合は検知できない。そのため、バッファを全て読み込んでしまい入力ストリームからバッファに読み込む時に、boundaryのサイズだけは常にバッファに残しておき、残りの部分に入力ストリームからデータを書き込む必要性がある。このboundaryの検索のために保持すべきサイズがkeepRegionであり、padは

pad = min(tail-head, keepRegion)

である。

ItemInputStream.makeAvailableの話に戻る。makeAvailableは読み込み可能なデータがバッファ内にない場合にデータを入力ストリームから読み込みバッファに入れて、読み込み可能なバイト数を返すメソッドである。

private int makeAvailable() throws IOException {
    // boundaryが見つかったので終了
    if (pos != -1) {
        return 0;
    }   

    // head〜tailのうち後ろ側のpad分をbufferの最初にもってくる
    // Move the data to the beginning of the buffer.
    total += tail - head - pad;
    System.arraycopy(buffer, tail - pad, buffer, 0, pad);

    // 有効なのは0〜padになる
    // Refill buffer with new data.
    head = 0;
    tail = pad;

    for (;;) {
        // bufferの有効ではない部分に入力から読み込む
        int bytesRead = input.read(buffer, tail, bufSize - tail);
        if (bytesRead == -1) {
            // The last pad amount is left in the buffer.
            // Boundary can't be in there so signal an error
            // condition.
            final String msg = "Stream ended unexpectedly";
            throw new MalformedStreamException(msg);
        }   
        if (notifier != null) {
            notifier.noteBytesRead(bytesRead);
        }   

        // tailを読んだ分だけ拡張する
        tail += bytesRead;

        // buffer内からboundaryを検索し、その位置をposに入れる
        // boundaryが見つからなければ、pos=-1になる
        findSeparator();
        // bufferから読み込み可能なデータを探して、読み込み可能なバイト数を返す
        int av = available();

        // 読み込み可能なデータがあるか、boundaryが見つかった場合はバイト数を返す
        if (av > 0 || pos != -1) {
            return av; 
        }   
    }   
}

available()は現在の読み込み可能なバイト数を返すが、ちょっと特殊で以下のようになっている。

public int available() throws IOException {
  if (pos == -1) {
    return tail - head - pad;
  }
  return pos - head;
}

buffer内にboundaryが見つかった場合はboundaryの開始インデックスまでのサイズを返すが、見つからなければ今有効なサイズ(tail-head)からboundaryに必要なサイズを除いたサイズを返している。boundaryのサイズ分はすぐには使えないと考えているようだ。

で、ループしているのはmakeAvailableのfor文の部分である。

このforループが以下のような条件で攻撃された時の動作を考えてみる。

  • boundaryを4092文字の文字列とし、
  • 入力ストリームがboundaryのない4097文字のバイト列とする。

初期状態は以下のようになっている

  • head=tail=0
  • bufferは用意されているが、0 fillされている
  • bufSize=4096(デフォルト)
  • boundaryにはCR,LF,-,-,boundaryの値が入っている。よって4096文字
  • keepRegionはboundary.lengthなので4096

すると、以下のように処理が進む

  1. 初期状態ではhead=tail=0となっている。
  2. input.readでバッファを読んでいき、tailも読んだサイズに拡張される。
  3. 初回でバッファのサイズ文読み込まれるとは限らないが、バッファサイズまで読み込まれない場合でも、boundaryが見つからずpos=-1になり、available()もtail-head-pad=tail-head-min(tail-head, 4096)=tail-head-(tail-head)=0を返すので、次のループでバッファ全体に読み込まれる
  4. バッファ全体を読み込んだ時には以下のようになり、ループ条件を抜けられない
    • head = 0, tail = 4096
    • pad = min(tail-head, 4096) = 4096
    • pos = -1 (boundaryは発見できない)
    • available() = tail-head-pad = 0
  5. その後も上記の状態のままinput.readしても0バイト読み込むだけなので終了しない

となって無限ループする。

commons fileuploadのv1.3.1ではこの問題が修正されており、以下のコードが追加されている。

@@ -331,9 +326,14 @@
 
         // We prepend CR/LF to the boundary to chop trailing CR/LF from
         // body-data tokens.
-        this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length];
         this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
+        if (bufSize < this.boundaryLength + 1) {
+            throw new IllegalArgumentException(
+                    "The buffer size specified for the MultipartStream is too small");
+        }
+        this.boundary = new byte[this.boundaryLength];
         this.keepRegion = this.boundary.length;
+
         System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
                 BOUNDARY_PREFIX.length);
         System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,

boundaryLengthはリクエスト指定したboundaryのサイズ+4なので4092バイト以上のboundaryが指定された場合はエラーになるようになった。

さて、大元のCVE-2016-3092の話に戻る。boundaryが4092バイト以上の場合は良いが4091バイトだったらどうか。
バッファいっぱいまで読み込んだ状態では

  • head=0,tail=4096
  • keepRegion=4091+4=4095
  • pad=min(tail-head, keepRegion)=min(4096,4095)=1

となり、availableは1を返す。ではmakeAvailableを読んでいる方は以下のコードになる

public int read(byte[] b, int off, int len) throws IOException {
  if (closed) {
    throw new FileItemStream.ItemSkippedException();
  }
  if (len == 0) {
    return 0;
  }
  int res = available();
  if (res == 0) {
    // resに1が入る
    res = makeAvailable();
    if (res == 0) {
      return -1;
    }
  }
  res = Math.min(res, len);
  // 1バイトしかコピーされない
  System.arraycopy(buffer, head, b, off, res);
  head += res;
  total += res;
  return res;
}

1バイトに対するSystem.arraycopyが発生する。リクエストボディが小さければ大したことないが1MB程度のリクエストを送るとこれを1バイトずつ処理して100万回近くループが発生してしまう。おそらくこれがCVE-2016-3092の脆弱性ではないかと思う。

This caused the file
upload process to take several orders of magnitude longer than if the
boundary length was the typical tens of bytes.

http://mail-archives.apache.org/mod_mbox/www-announce/201606.mbox/%3C6223ece6-2b41-ef4f-22f9-d3481e492832@apache.org%3E

と無限ループになるわけではないし、今回の修正のdiffをみるとコンストラクタで最低でもboundaryLengthの2倍をとるようになっているという点とも合致する。

@@ -325,12 +325,6 @@
         if (boundary == null) {
             throw new IllegalArgumentException("boundary may not be null");
         }
-
-        this.input = input;
-        this.bufSize = bufSize;
-        this.buffer = new byte[bufSize];
-        this.notifier = pNotifier;
-
         // We prepend CR/LF to the boundary to chop trailing CR/LF from
         // body-data tokens.
         this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
@@ -338,6 +332,12 @@
             throw new IllegalArgumentException(
                     "The buffer size specified for the MultipartStream is too small");
         }
+
+        this.input = input;
+        this.bufSize = Math.max(bufSize, boundaryLength * 2);
+        this.buffer = new byte[this.bufSize];
+        this.notifier = pNotifier;
+
         this.boundary = new byte[this.boundaryLength];
         this.keepRegion = this.boundary.length;

こうすることで、バッファの半分程度はavailable()で返るようになり、デフォルトのバッファのサイズが4KBなので最低でも2KB程度ずつは処理されることになる。

プリンシプル オブ プログラミング

良いプログラミングに必要な原則、手法をまとめた本。著者は具体例やコードを用いないというポリシーで書いているのだが、やはり具体例がないと頭に入ってこない感じがしてよくないな。

set -eはどこまで有効なのか

シェルスクリプト(bash)はコマンドが失敗しても次のコマンドを実行してしまうので怖い→set -eしておけという話はよくあるが、実際どこまで有効なのか。

基本

コマンドの戻り値が0になった時にシェルを終了する

#!/bin/bash
set -e
echo "before false"
false
echo "after false"
./basic.sh 
before false
# falseで0以外を返したので、シェルが終了しechoが実行されない

if

ifの条件が0以外を返してもシェルは終了しない

#!/bin/bash
set -e
echo "before if"
if [ 1 = 0 ]; then
  echo "in if"
fi
echo "after if"
$ ./if.sh
before if
after if # ifの条件は0以外を返しているがシェルは終了していない

while,until

whileやuntilの条件が0以外を返してもシェルを終了しない

#!/bin/bash
set -e

i=0
echo "start while"
while [ $i -lt 5 ]
do
  echo $i
  i=$((i+1))
done
echo "end while"

echo "start until"
until [ $i -lt 0 ]
do
  echo $i
  i=$((i-1))
done
echo "end until"
$ ./while.sh 
start while
0
1
2
3
4
end while # i=5の時にwhileの条件が0以外を返すがシェルは終了していない
start until
5
4
3
2
1
0
end until # i=-1の時にuntilの条件が0以外を返すがシェルは終了していない

&&や||

&&や||の前のコマンドが0以外を返していてもシェルを終了しない

#!/bin/bash
set -e
echo "start and or"
rm hoge || echo hoge
rm fuga && echo fuga
echo "end and or"
$ ./andor.sh
start and or
rm: hoge: No such file or directory
hoge              # rm hogeが0以外を返していても後ろのコマンドも実行される
rm: fuga: No such file or directory
end and or    # rm fugaが0以外を返していてもシェルは終了していない

パイプライン

パイプラインの途中で0を返しても、最後まで実行される。

$ cat pipe.sh 
#!/bin/bash
set -e
echo "hoge" | grep fuga | wc -l
echo ${PIPESTATUS[*]}
./pipe.sh 
       0           # grep fugaが0以外を返していてもwc -lが実行されている
0 1 0            # パイプラインの各コマンドのステータス

シーケンス(;)

セミコロンで繋がれたコマンドの連続の場合は途中で0を返せばシェルを終了する

#!/bin/bash
set -e
echo "before seq"; rm hoge; echo "after seq";
./sequence.sh 
before seq
rm: hoge: No such file or directory
# rm hogeが0以外を返したのでechoの前にシェルが終了した

サブシェル

サブシェルもset -eが有効になる。以下の場合は、rm hogeでサブシェルが終了し、その結果(...)が0以外を返して親のシェルも終了する。

#!/bin/bash
set -e
echo "before subshell"
(rm hoge; echo "in subshell")
echo "after subshell"
$ ./subshell.sh 
before subshell
rm: hoge: No such file or directory

関数

関数の内部で0以外を返していれば、関数の途中でシェルを終了する。

#!/bin/bash

hoge() {
  false
  echo "in function"
}

set -e
echo "before function hoge"
hoge
echo "after function hoge"
$ ./function.sh 
before function hoge
# hoge内のfalseで0以外を返したのでシェルが終了した

まあman見ればよかったのだけども。

$ bash --version
GNU bash, バージョン 4.3.11(1)-release (x86_64-pc-linux-gnu)
...
$ man bash
...
-e    Exit  immediately if a pipeline (which may consist of a
        single simple command), a list, or a  compound  command
        (see  SHELL GRAMMAR above),  exits with a non-zero sta‐
        tus.  The shell does not exit if the command that fails
        is  part  of  the  command list immediately following a
        while or until keyword, part of the test following  the
        if or elif reserved words, part of any command executed
        in a && or || list except  the  command  following  the
        final && or ||, any command in a pipeline but the last,
        or if the command's return value is being inverted with
        !.  If a compound command other than a subshell returns
        a non-zero status because a command failed while -e was
        being ignored, the shell does not exit.  A trap on ERR,
        if set, is  executed  before  the  shell  exits.   This
        option  applies  to the shell environment and each sub‐
        shell environment  separately  (see  COMMAND  EXECUTION
        ENVIRONMENT  above),  and  may  cause subshells to exit
        before executing all the commands in the subshell.

mac os xbashのmanはmanが古いのか嘘が書いてあった。パイプラインやサブシェルの話は書いていない

$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin15)
Copyright (C) 2007 Free Software Foundation, Inc.
$ man bash
...
-e    Exit immediately if a simple command (see SHELL  GRAMMAR
        above) exits with a non-zero status.  The shell does not
        exit if the command that fails is part  of  the  command
        list  immediately  following  a  while or until keyword,
        part of the test in an if statement, part of a && or  ||
        list, or if the command's return value is being inverted
        via !.  A trap on ERR, if set, is  executed  before  the
        shell exits.

紙のコミックとKindleコミックで発売日が同じもの 2016年08月分

あずきの地!1 (NextcomicsF)

あずきの地!1 (NextcomicsF)

あずきの地!2 (NextcomicsF)

あずきの地!2 (NextcomicsF)

7thGARDEN 6 (ジャンプコミックスDIGITAL)

7thGARDEN 6 (ジャンプコミックスDIGITAL)

コントラスト88 2 (ジャンプコミックスDIGITAL)

コントラスト88 2 (ジャンプコミックスDIGITAL)

ヤミアバキクラウミコ 2 (ジャンプコミックスDIGITAL)

ヤミアバキクラウミコ 2 (ジャンプコミックスDIGITAL)

BUNGO―ブンゴ― 6 (ヤングジャンプコミックスDIGITAL)

BUNGO―ブンゴ― 6 (ヤングジャンプコミックスDIGITAL)

テラフォーマーズ 18 (ヤングジャンプコミックスDIGITAL)

テラフォーマーズ 18 (ヤングジャンプコミックスDIGITAL)

元ヤン 5 (ヤングジャンプコミックスDIGITAL)

元ヤン 5 (ヤングジャンプコミックスDIGITAL)

てをつなごうよ 1 (マーガレットコミックスDIGITAL)

てをつなごうよ 1 (マーガレットコミックスDIGITAL)

アナグラアメリ 2 (マーガレットコミックスDIGITAL)

アナグラアメリ 2 (マーガレットコミックスDIGITAL)

チョコタン! 11 (りぼんマスコットコミックスDIGITAL)

チョコタン! 11 (りぼんマスコットコミックスDIGITAL)

ハンキー・ドリー 2 (マーガレットコミックスDIGITAL)

ハンキー・ドリー 2 (マーガレットコミックスDIGITAL)

歌うたいの黒うさぎ 10 (マーガレットコミックスDIGITAL)

歌うたいの黒うさぎ 10 (マーガレットコミックスDIGITAL)

雛鳥のワルツ 6 (マーガレットコミックスDIGITAL)

雛鳥のワルツ 6 (マーガレットコミックスDIGITAL)

ケン・トンプソンのトロイの木馬

ケン・トンプソンのトロイの木馬は、ケン・トンプソンがチューリング賞を受賞したときのスピーチ「Reflections on Trusting Trust」で発表したCコンパイラのハックの話で、以下の様なものである。

1. loginコマンドに特定のユーザであればログインを許可するようなコードを埋め込む。
2. 1はloginコマンドのソースコードを読めばバレるので、コンパイラに、loginコマンドをコンパイル時に1のコードを埋め込むようなコードを埋め込む
3. 2はコンパイラソースコードを読めば分かるので、コンパイラに、コンパイラソースコードコンパイルする時に2のコード埋め込むようなコードを埋め込み、かつ自分自身と同じような動作するようにコードを埋め込む

3のコンパイラは一度不正なコンパイラバイナリが生成されると、正常なソースコードを渡されても不正なコンパイラバイナリを生成するので、一定期間不正なものを渡しておいて、あとは正常なソースコードに戻してあげれば、利用者は不正なコンパイラコンパイルしてしまった後は、ソースコード上は正しく見えるコンパイラコンパイルすると不正なコンパイラになってしまい、非常に気付きづらい。

ところで、ケン・トンプソンがこのトロイの木馬を実際に実装して配布していたのではという疑惑があるが、ネットを探す限り真偽は明確ではない。

Jargon Fileによると

The Turing lecture that reported this truly moby hack was later published as “Reflections on Trusting Trust”, Communications of the ACM 27, 8 (August 1984), pp. 761--763 (text available at http://www.acm.org/classics/). Ken Thompson has since confirmed that this hack was implemented and that the Trojan Horse code did appear in the login binary of a Unix Support group machine. Ken says the crocked compiler was never distributed. Your editor has heard two separate reports that suggest that the crocked login did make it out of Bell Labs, notably to BBN, and that it enabled at least one late-night login across the network by someone using the login name “kt”.

back-door
  • ケン・トンプソンはこのトロイの木馬を実装してUNIXサポートグループのマシンに入っていたことは認めた
  • しかし、配布したことは認めていない
  • Jargon Fieの編集者によると、BBNへ配布されたこと、深夜にktというログイン名の誰かからネットワーク経由でログインされたという2つの別々の報告がある。

また、こちらのStackExchangeのスレッドによると

From: Ken Thompson
Date: Wed, Sep 28, 2011 at 6:27 PM
Subject: Re: Was compiler from "Reflections" ever built or distributed?
To: Ezra Lalonde


build and not distributed.

On Wed, Sep 28, 2011 at 11:35 AM, Ezra Lalonde wrote:
> Hi Ken,
>
> I've seen various sources on the internet claiming that the "trojan horse"
> compiler you mentioned in your talk "Reflections on Trusting Trust" was
> actually built, and some further claiming that it was distributed.
>
> I'd like to know if these claims are valid.
>
> Thanks for your time.
>
> Cheers,
> Ezra Lalonde

とメールで確認をとると、「ビルドしたが配布していない」という回答だったそうだ。

しかし、最近読んでいる「Unix考古学」では普通にサポートに利用されていたらしく書かれてあったので、ちょっと気になった。

そのため当時のUnixディストリビューションには、「ken」というユーザーアカウントが残されていました。後のTuring Award受賞講演で暴露した「トロイの木馬」は、この時期に使われていたものだと推測されます。


少し別の話になるが、この発表の中で少しhackerの道徳に関して論じている。

I would like to criticize the press in
its handling of the "hackers," the 414 gang, the Dalton
gang, etc. The acts performed by these kids are vandalism
at best and probably trespass and theft at worst. It
is only the inadequacy of the criminal code that saves
the hackers from very serious prosecution

The
act of breaking into a computer system has to have the
same social stigma as breaking into a neighbor's house.
It should not matter that the neighbor's door is unlocked.
The press must learn that misguided use of a
computer is no more amazing than drunk driving of an
automobile.

当時は414 gangという青少年達がコンピュータに不正アクセスしており、実際にデータの破壊行為も行っていた。メディアは彼らをヒーローとして持ち上げている部分もあり、それで批判しているようだ。
この時代の人なので牧歌的なのかなと勝手に思っていたのだが、かなり手厳しい。

紙のコミックとKindleコミックで発売日が同じもの 2016年07月分

あの娘はヤリマン 2 (ジャンプコミックスDIGITAL)

あの娘はヤリマン 2 (ジャンプコミックスDIGITAL)

この音とまれ! 12 (ジャンプコミックスDIGITAL)

この音とまれ! 12 (ジャンプコミックスDIGITAL)

バイバイ人類 1 (ジャンプコミックスDIGITAL)

バイバイ人類 1 (ジャンプコミックスDIGITAL)

ファイアパンチ 1 (ジャンプコミックスDIGITAL)

ファイアパンチ 1 (ジャンプコミックスDIGITAL)

殺せんせーQ! 1 (ジャンプコミックスDIGITAL)

殺せんせーQ! 1 (ジャンプコミックスDIGITAL)

Challenge Up! 1 (マーガレットコミックスDIGITAL)

Challenge Up! 1 (マーガレットコミックスDIGITAL)

それでも君が 3 (マーガレットコミックスDIGITAL)

それでも君が 3 (マーガレットコミックスDIGITAL)

なないろ革命 5 (りぼんマスコットコミックスDIGITAL)

なないろ革命 5 (りぼんマスコットコミックスDIGITAL)

ケダモノ彼氏 13 (マーガレットコミックスDIGITAL)

ケダモノ彼氏 13 (マーガレットコミックスDIGITAL)

食べ部にイイネ! (マーガレットコミックスDIGITAL)

食べ部にイイネ! (マーガレットコミックスDIGITAL)

蜃気楼家族5 (幻冬舎単行本)

蜃気楼家族5 (幻冬舎単行本)

しりこだま! 1 (ジャンプコミックスDIGITAL)

しりこだま! 1 (ジャンプコミックスDIGITAL)

遊☆戯☆王ARC-V最強デュエリスト遊矢!! 1 (ジャンプコミックスDIGITAL)

遊☆戯☆王ARC-V最強デュエリスト遊矢!! 1 (ジャンプコミックスDIGITAL)

ImageMagickの脆弱性(ImageTragick)

piyokangoさんが詳しいが自分も少し調べたので。

d.hatena.ne.jp
ImageTragick

どんな脆弱性

外部からの入力により、意図せずに、ファイルを読みこんだり、ファイルを移動したり、削除したり、特定のURLにアクセスしたり、任意のコードを実行可能な脆弱性

どういうアプリが攻撃される?

画像をアップロードしてImageMagickを使って変換するみたいなサービスが狙われるかな。

どういう仕組みで攻撃しているのか

ImageMagickはいろんな種類のファイルを処理できるが、そのためにcoderとdelegateという機能がある。coderはライブラリとして各種ファイルを変換する機能、delegateは適切なcoderがなかった時に、外部コマンドを用いて変換する機能である。

CVE-2016-3714(コード実行の脆弱性)については、delegateの機能で外部コマンドを呼び出す時にsystem関数を利用していたが、shellの特殊文字のエスケープを実施していなかったことが主な原因。

例えば、

convert https://www.imagemagick.org/image/wizard.png wizard.png

を実行すると、imagemagickのサイトからwizard.pngをダウンロードしてwizard.pngというファイルに保存している。

内部的には次のような処理を行っている。
ImageMagickはconvertの変換元(第一引数)をHTTPSという種類のファイルとみなす。HTTPSのcoderは存在しないので、delegateを探す。delegateはdelegates.xmlに記述されており、CentOS6では以下のものが該当する

<delegate decode="https" command="&quot;curl&quot; -s -k -o &quot;%o&quot; &quot;https:%M&quot;"/>

ImageMagickはcommandの部分のコマンドを実行する。%Mには入力ファイル名が渡される。しかし%Mを置換する際にshellの特殊文字のエスケープを実施していないので、

convert 'https://www.imagemagick.org/image/wizard.png"|ls "-la' wizard.png

を実行すると

"curl" -s -k -o "wizard.png" "https://www.imagemagick.org/image/wizard.png"|ls "-la"

が実行され、lsも実行されることになる。

これだけだと、普通のアプリだと外部の入力をそのままconvertの引数にしないから大丈夫じゃないかと思える。が、この処理を画像ファイルの中に隠すことができる。

ImageMagickにはSVGとMVG(ImageMagick独自のテキストベースの画像フォーマット)のcoderがあり、それらのフォーマットは外部のURLやファイルを参照できる。例えば以下のMVGファイルは外部URLhttps://example.com/image.jpgを含む。

push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg)'
pop graphic-context

この外部URL参照に対してもcoder/delegateが実行される。今回の場合はHTTPSフォーマットだとみなされ、HTTPSdelegate commandが実行され、任意のプロセスを起動できる。

さらに、ImageMagickはそのファイルがどのフォーマットのファイルかについて

  1. マジックバイト
  2. format:filename形式のprefix
  3. 拡張子(filename.suffix)

の優先順位でチェックする。SVGやMVGはマジックバイトがチェックされるので、拡張子pngなどを指定していたとしても、中身がSVG/MVGならばSVG/MVGと解釈されてしまう。

そのため、MVGファイルをアップロードする→convertでファイルを処理しようとするとMVGのcoderが実行される→URLを参照している部分でコードを実行可能ということになる。

その他の脆弱性についてだが、shellコマンドのエスケープの問題はないものの、これらもMVG内に外部のファイルやURLを参照することにより、ファイル/URLの読み書きを行ってしまうことが原因となっている。

修正版は?

最新版(7.0.1-1)をインストールすると防げるようである。

実際に動かしてみると一部しか防げない(×は防げていない)のでpolicy.xmlで防ぐ必要がある。

CVE 内容 防げる?
CVE-2016-3714 delegateによるRCE脆弱性
CVE-2016-3718 SSRF(URLに対するGET) ×
CVE-2016-3715 ephemeralプロトコルによるファイルの削除 ×
CVE-2016-3716 mslプロトコルによるファイルの移動 *1
CVE-2016-3717 labelプロトコルによるファイルの読み込み ×

回避策は?

脆弱性公式サイトによると以下のどちらかで防げる。

  1. マジックバイトをチェックする
  2. policy.xmlに以下を指定
<policymap>
  <policy domain="coder" rights="none" pattern="EPHEMERAL" />
  <policy domain="coder" rights="none" pattern="URL" />
  <policy domain="coder" rights="none" pattern="HTTPS" />
  <policy domain="coder" rights="none" pattern="MVG" />
  <policy domain="coder" rights="none" pattern="MSL" />
  <policy domain="coder" rights="none" pattern="TEXT" />
  <policy domain="coder" rights="none" pattern="SHOW" />
  <policy domain="coder" rights="none" pattern="WIN" />
  <policy domain="coder" rights="none" pattern="PLT" />
</policymap>

coderとかdelegateの周りの処理の流れがよくわからん

私もよくわかっていないが、以下のような感じかな

  1. 仕組みのところに書いた優先度でフォーマットを決める
    • マジックバイトのチェックはMagicCore/magic.cのMagicMapの値を読むとなんとなくわかる
    • magic.xmlにもカスタマイズしたものを指定可能だけども
  2. フォーマットに対応するcoderを探し、存在すればcoderを実行する
    • どのフォーマットがどのcoderに該当するかはMagicCore/coder.cのCoderMapを読むとなんとなくわかる
  3. coderが存在しなければdelegateを実行する
    • デフォルトのdelegateはconfig/delegates.xml.inに書かれてある

RedHatの回避策は微妙?

ImageMagick Filtering Vulnerability - CVE-2016-3714のResolveのMitigationのことだよね。うん、これは微妙だ。RHEL 5では使えないし、明示的にHTTPとFTPを禁止している理由が判らない。私がコードを眺めた限りでは、HTTPとFTPはURLが提供しているようだ。やれやれ。

policy.xmlが使えない古いImageMagickでImageTragickを回避する (2) - Qiita

私がコードを読んだ感じだとcoderのpolicyチェックはcoder名に対して行うのではなく、フォーマットに対して行うのでそんなに悪くない気がするかな。MagicCore/constitute.cのReadImage関数の部分

  if (IsRightsAuthorized(domain,rights,read_info->magick) == MagickFalse)
    {    
      errno=EPERM;
      (void) ThrowMagickException(exception,GetMagickModule(),PolicyError,
        "NotAuthorized","`%s'",read_info->filename);
      read_info=DestroyImageInfo(read_info);
      return((Image *) NULL);
    }

read_info->magickにはcoder名ではなくフォーマット名が入る。

実際にMVG内にHTTPのURLを書いた場合はHTTPのcoderをnoneにしないと防げなかった。

ただ、そもそもMVGをnoneにした時点で防げる話ではあるのだけども。

修正内容はどんなもの?

いまいち理解しきれていないが今回の脆弱性に関して7.0.1-1までに以下の修正が入ったっぽい。

その後の修正

今回の脆弱性関係でまだいくつか修正しているっぽい。

*1:なぜ防げているのかよくわからない