Arang.dev
検索

JSP commons-fileupload WAF Bypass

CCE2019 ENKIの問題を通して学ぶ、JSP commons-fileupload WAFフィルタリング回避テクニック

#web-security#waf-bypass#file-upload#jsp
🌐 翻訳済み

この記事はAIによって韓国語から機械翻訳されています。原文と表現が異なる場合があります。

原文(韓国語)を読む

JSP & Spring Frameworkで構築されたWebサービスにおいて、ファイルアップロード機能を実装する際、最も一般的に使用されるモジュールがcommons-fileuploadです。

악意のあるファイルアップロードの脆弱性を突き攻撃を成功させるには、サーバー側でWebサービスが解釈・実行可能な拡張子(例: jsp, jspx, jspl, jsw ...etc..)を持つファイルをアップロードし、サーバー内で任意のコードを実行できる必要があります。

しかし、多くのサービスでは拡張子(ext)の検証が行われており、この処理をWAF(Web Application Firewall)側で実施しているケースも少なくありません。

また、拡張子の検証を行った後にファイル名をランダム化(randomizing)し、セキュリティを確保するのが、ファイルアップロードの脆弱性に対する一般的な対策です。


しかし、もし拡張子の検証をアプリケーションコード側ではなくWAF側でのみ行い、かつランダムに生成されたファイル名が攻撃者に露呈してしまう場合(この状況はかなり多くのケースで見受けられます)、commons-fileuploadモジュールの実装上の特徴を利用して、これを回避することが可能です。

@Override
 protected void doPost(HttpServletRequest request, HttpServletResponse response)
 throws ServletException, IOException {
 
 response.setContentType("text/html; charset=UTF-8");
 request.setCharacterEncoding(CHARSET);
 PrintWriter out = response.getWriter();
 
 File attachesDir = new File(ATTACHES_DIR);
 
 DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
 fileItemFactory.setRepository(attachesDir);
 fileItemFactory.setSizeThreshold(LIMIT_SIZE_BYTES);
 ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);
 
 try {
 List<FileItem> items = fileUpload.parseRequest(request);
 for (FileItem item : items) {
 if (item.isFormField()) {
 System.out.printf("파라미터 명 : %s, 파라미터 값 : %s \n", item.getFieldName(), item.getString(CHARSET));
 } else {
 System.out.printf("파라미터 명 : %s, 파일 명 : %s, 파일 크기 : %s bytes \n", item.getFieldName(),
 item.getName(), item.getSize());
 if (item.getSize() > 0) {
 Object fileName = new File(item.getName()).getName();
 fileName = this.makeFn((String)fileName) + "." + this.getExt((String)fileName); // makeFn -> make random filename
 String filePath = uploadPath + File.separator + (String)fileName;
 File storeFile = new File(filePath);
 item.write(storeFile);
 }
 }
 }
 out.println("<h1>파일 업로드 완료</h1>");
 } catch (Exception e) {
 // 파일 업로드 처리 중 오류가 발생하는 경우
 e.printStackTrace();
 out.println("<h1>파일 업로드 중 오류가 발생하였습니다.</h1>");
 }
 }
 
//출처: https://dololak.tistory.com/720 [코끼리를 냉장고에 넣는 방법]
private String getExt(String fileName) {
 if (fileName.lastIndexOf(".") == -1)
 return "noext"; 
 fileName = fileName.substring(fileName.lastIndexOf(".") + 1).trim();
 if ("".equals(fileName))
 return "noext"; 
 return fileName.substring(fileName.lastIndexOf(".") + 1);
 }

上記のコードは、Googleでcommons-fileuploadと検索した際に表示される「基本的なサンプルコード」に、ファイル拡張子の検証ロジックを追加したものです。

実際のコード上では、WAFで拡張子を検証し、.jpgや.pngなどの許可された拡張子のみをホワイトリストで管理していれば、セキュリティ上の脆弱性は発生しないように見えます。実際、私自身もこの手法を知るまでは、そのような状況下での脆弱性エクスプロイトは不可能だと考えていました。

しかし、問題はcommons-fileuploadモジュールが、バージョン1.3からRFC2047を지원するという点から始まります。

RFC2047はMIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Textに関する仕様であり、Non-ASCIIテキストに対してリクエスト送信側がエンコーディング方式を指定して送信し、受信側がそれをデコードして解釈するという内容が主となっています。

commons-fileuploadモジュールでこれがどのように適用されているか、簡潔なコードフローで確認してみると以下のようになります。

  • 上記コード内のList<FileItem> items = fileUpload.parseRequest(request);部分
  • public List<FileItem> parseRequest(HttpServletRequest req)にて、内部的に**private String getFileName(String pContentDisposition)**を呼び出し
  • private String getFileName(String pContentDisposition)にて、public Map<String, String> parseを呼び出し
  • public Map<String, String> parseにて、**public static String decodeText(String text)**を呼び出し
  • public static String decodeText(String text)にて、**private static String decodeWord(String word)**を呼び出し、RFC 2047仕様に基づきエンコードされたパラメータのデコードを実行

という流れで処理が進みます。

RFC2047で定義されているエンコーディング形式は=?" charset "?" encoding "?" encoded-text "?=です。

このうち、encoding方式に関する説明は以下の通りです。

4. Encodings
 
 Initially, the legal values for "encoding" are "Q" and "B". These
 encodings are described below. The "Q" encoding is recommended for
 use when most of the characters to be encoded are in the ASCII
 character set; otherwise, the "B" encoding should be used.
 
 ...중략...
 
 4.1. The "B" encoding
 
 The "B" encoding is identical to the "BASE64" encoding defined by RFC

エンコーディングは、Quoted-Printable (Q) と Base64 (B) をサポートしています。これを利用することで、some_webshell.jspにおける.jspのような文字列を検知するWAFを回避することが可能です。

しかし、依然としてWAFによる「末尾の拡張子を検証するロジック」は回避できていません。

これは、commons-fileuploadにおけるRFC2047の実装上の特徴を利用することで回避できます。

decodeText() 메서드 중 인코딩 형식의 문자열 파싱 부분を確認すると、int encodedTextPos = word.indexOf(ENCODED_TOKEN_FINISHER, encodingPos + 1);としてencodedTextPosを設定しており、その後のコードで「エンコードされた領域のみ」を取得してデコードし、ファイル名として採用しています。

そのため、=?UTF8?B?c29tZV93ZWJzaGVsbC5qc3A=?=.jpgのような形式で、エンコード領域が終わった直後に.jpgを挿入すると、実際にJSP側でファイル名をパースする際、この部分(.jpg)が無視されます。


ここまで来れば、WAFが実行する「1. '.jsp'のような文字列を含むかの検査」や、「2. lastIndexOf('.')」などによるアップロード脆弱性検査をすべて回避することができます。

その後、JSPコード側では正常にsome_webshell.jspとしてファイルが生成されるため、Webシェルのアップロードに成功することになります。


個人的にもファイルアップロード脆弱性の診断時、commons-fileuploadモジュールが多く使用されていることを感じていましたが、このような手法が存在することは知らず、内容を見て非常に驚きました。そのため、この情報を知ってすぐに急いで記事を作成し、公開することにしました。

本記事は、2019年 CCE(サイバー攻撃防御大会)に出題されたENKIの公式Write-upを参照して作成されました。

参考文献 :https://enki.co.kr/blog/2020/02/27/cce_writeup.html?fbclid=IwAR0ztC_wshd_DHvIA-HBMh_F99TdqkPiqyGyBu_WfP6Id-2TPTPPp_uPkZY

コメント

コメントを読み込み中...
JSP commons-fileupload WAF Bypass | Arang