※内容的にはかなり浅くて緩い記事なので、参考程度の扱いでお願いします。また、以下はmsakamoto-sfのプライベートの活動になりますので、所属する組織の公式見解ではありませんのでご了承ください。
こんなのが出てまして、Apache Commons FileUpload については実際に使ったこともありましたので、ちょっと気になりました。
commons-fileuploadについては、 1.3.1 で修正されてるということなので、どんな修正されたのか?↑の参考資料中に、開発者自身のメーリングリスト上での発言がありました。
んー、なんかよく分かんないんですが、バッファサイズのチェックを強化したとかなんとか。
Content-Typeとかboundaryっぽい感じがしますので、関連RFCをざっと復習。
commons-fileupload:
あとよくわかんないのが、JPCERTの方のページに回避策として紹介されてる「Content-Type ヘッダのサイズを 4091 より小さいサイズに制限する」ですね。4091ってどっから出てきたのか謎です。
実際にSVNリポジトリ眺めてみると、r1565143 で、以下のコミットログでそれっぽい修正がされてます。
Fix CVE-2014-0050. Specially crafted input can trigger a DoS if the buffer used by the
<code>MultipartStream</code> is not big enough. When constructing <code>MultipartStream</code>
enforce the requirements for buffer size by throwing an <code>IllegalArgumentException</code>
if the requested buffer size is too small. This prevents the DoS.
このコミットでのメインとなる修正箇所は src/main/java/org/apache/commons/fileupload/MultipartStream.java のコンストラクタ部分です。
修正前:
public MultipartStream(InputStream input, byte[] boundary, int bufSize, ProgressNotifier pNotifier) { 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.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); head = 0; tail = 0; }
修正後:
public MultipartStream(InputStream input, byte[] boundary, int bufSize, ProgressNotifier pNotifier) { 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; 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, boundary.length); head = 0; tail = 0; }
bufSizeで渡されたバッファサイズが、boundaryLength+1より小さいと例外を投げるようになりました。
・・・この修正箇所が、DoSの発生箇所とイコールには思えませんでした。
深くコードを追ってる時間も無いので、もう直接 1.3 版と 1.3.1 版で実際に使って比べてみます。
とりあえず動けば良いので、Web上のサンプルを切り貼りしてこんなServlet作ってみました。
public class FileUploadSampleServlet extends HttpServlet { private static final long serialVersionUID = 7896611580723129112L; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024 * 1024); ServletContext servletContext = this.getServletConfig() .getServletContext(); File repository = (File) servletContext .getAttribute("javax.servlet.context.tempdir"); factory.setRepository(repository); ServletFileUpload upload = new ServletFileUpload(factory); upload.setSizeMax(1024 * 1024 * 1024); try { List<FileItem> items = upload.parseRequest(req); Iterator<FileItem> iter = items.iterator(); while (iter.hasNext()) { FileItem item = iter.next(); if (item.isFormField()) { String name = item.getFieldName(); String value = item.getString(); System.out.println("[" + name + "]=[" + value + "]"); } else { String fieldName = item.getFieldName(); String fileName = item.getName(); String contentType = item.getContentType(); boolean isInMemory = item.isInMemory(); long sizeInBytes = item.getSize(); byte[] data = item.get(); System.out.println("fieldName=" + fieldName); System.out.println("fileName=" + fileName); System.out.println("contentType=" + contentType); System.out.println("isInMemory=" + isInMemory); System.out.println("sizeInBytes=" + sizeInBytes); System.out.println("length=" + data.length); } } } catch (FileUploadException e) { e.printStackTrace(); } resp.setContentType("text/html; charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("uploaded."); } }
はい。で、commons-fileuploadの1.3で、動かしてみます。こんなリクエストをBurpとかお好みのHTTP Proxyソフトで組み立てて送ってあげます。
POST /(サンプルServletのpath) HTTP/1.1 Host: xxxxx User-Agent: yyyyy Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Cache-Control: max-age=0 Content-Type: multipart/form-data; boundary=BOUNDARYABCABC Content-Length: 2909 --BOUNDARYABCABC Content-Disposition: form-data; name="t1" abc --BOUNDARYABCABC Content-Disposition: form-data; name="f1"; filename="corkboard.txt" Content-Type: text/plain abcdefg --BOUNDARYABCABC--
ちゃんと受信できれば正常動作確認OKです。
ここで 1.3 版で、"BOUNDARYABCABC" の部分を 4091 バイトの長さの文字列にして送信すると、これは正常に動きます。
ところが、4092 バイトの長さにして送ると、リクエストが返ってきません。
どうやら、org.apache.commons.fileupload.util.Streams#copy() の中のread系の処理で待ちが発生しているらしく、Threadが解放されません。何回も送りつけるとその分だけThreadが増えていってしまい、リソースが枯渇します。
これが 1.3.1 版になると、以下の様な例外が発生します。エラーとはなりますが、処理は進みますのでThreadは開放され、リソースが枯渇しなくなります。
org.apache.commons.fileupload.FileUploadBase$InvalidContentTypeException: The boundary specified in the Content-type header is too long at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:997) at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310) at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334) at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115) at net.glamenvseptzen.cve20140050.FileUploadSampleServlet.doPost(FileUploadSampleServlet.java:40) at javax.servlet.http.HttpServlet.service(HttpServlet.java:647) at javax.servlet.http.HttpServlet.service(HttpServlet.java:728) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:936) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1004) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603) at java.lang.Thread.run(Thread.java:722) Caused by: java.lang.IllegalArgumentException: The buffer size specified for the MultipartStream is too small at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:338) at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:367) at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:995) ... 22 more Caused by: java.lang.IllegalArgumentException: The buffer size specified for the MultipartStream is too small at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:338) at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:367) at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:995) at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310) at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334) at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115) at net.glamenvseptzen.cve20140050.FileUploadSampleServlet.doPost(FileUploadSampleServlet.java:40) at javax.servlet.http.HttpServlet.service(HttpServlet.java:647) at javax.servlet.http.HttpServlet.service(HttpServlet.java:728) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:936) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1004) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603) at java.lang.Thread.run(Thread.java:722)
さて、挙動の変化は確認できましたので、あとは「4091バイト」です。
※DoSが実際に発生する処理の特定までは今回はタイムアップなので諦めます。なので「不完全燃焼」です。
サンプルコードみたいに普通の流れでmultipartを処理するとき、MultipartStreamのコンストラクタは以下が使われました。
public MultipartStream(InputStream input, byte[] boundary) { this(input, boundary, DEFAULT_BUFSIZE, null); }
この "DEFAULT_BUFSIZE" というのが、4096 に設定されています。委譲されたコンストラクタの方では、boundaryLengthの計算で以下のコードを使ってます。
this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
BOUNDARY_PREFIXというのは、CR + LF + "--" の4バイトです。
つまり、4091というのはboundaryLengthが 4091 + 4 で 4095 になる境界値となるわけで、いかにも怪しそうです。
コードの深追いはタイムアップでやりたくないので、このへんで止めておきますが、バッファサイズ周りのズレが原因で、どこかでずっとread待ちになってしまい、処理が止まってしまったのが原因のようです。
そこで、コンストラクタの段階で不正なバッファサイズが指定されたら例外を発生させ、read待ちにならないような修正がなされたようです。
対策ですが、commons-fileuploadを単体で使っているプロジェクトであれば、1.3.1にアップデートするのが良いと思います。
もちろん、commons-fileuploadを使っていないプロジェクト or Tomcat7 以降(=Servlet 3.0以降) でも @MultipartConfig を使っていない箇所については対策はそもそも不要かなと・・・。まさか@MultipartConfigを使っていても使ってなくても動いてしまったりは・・・しないですよね・・・(未確認)。
アップデートできないなどの場合は、JPCERTの回避策にあるように、HTTPリクエストのContent-Typeをチェックしてboundaryが4091バイト以下になるよう制限を加えることになるでしょう。具体的にはリクエストのContent-Typeをチェックしてboundaryが4092バイト以上ならそこで異常終了させてFilterChainをそれ以上動かさない、ようなServletFilterを追加して、multipart処理を行うServletが動く前にそのFilterを通すように修正する、というのが思い浮かびました。
IP制限つけてログインセッションチェック必須の、社内専用ツールの場合などは、攻撃者となりうるのが著しく範囲が狭まるような状況なので、特に修正せずリスクを受け入れる、というような判断をするのもあり得ると思います。
DoS発生箇所の特定とロジックの解説まで踏み込めなかったので不完全燃焼ですが、今回は以上です。
コメント