XMLRPCをフィルタリングする

WordPressのXMLRPCって便利なのですけどpingbackスパムやログイン攻撃の的になるんですよね。
スカイプでそんな事を話していたら、同じくWordPressを持ってる人に「まじ?どうすればいいの?」と言われたので、うちのブログで使っているヤツのコードを組み直して渡しました。

コードはこれ

<?php
 
class kerberos_xmlrpc_filter {
    private $config = [
        'disable-rpc' => false,
        'remove-pingback-header' => false,
        'filter' => [
            'enable' => true,
            'process' => 1, // 0 = remove method and continue WP, 1 = HTTP 403, 2 = exit
            'message' => [ // process = 1 or 2
                'enable' => true,
                'status' => 200,
                'content-mime' => 'text/plain',
                'message' => 'You don\'t have permission to access on this server.'
            ],
            'methods' => [
                // WordPress API
                'demo.sayHello'            => ['enable' => true,    'process' => 0],
                'system.multicall'        => ['enable' => false,    'process' => 0],
                'wp.getUsersBlogs'        => ['enable' => false,    'process' => 0],
                'wp.newPost'            => ['enable' => false,    'process' => 0],
                'wp.editPost'            => ['enable' => false,    'process' => 0],
                'wp.deletePost'            => ['enable' => false,    'process' => 0],
                'wp.getPost'            => ['enable' => true,    'process' => 0],
                'wp.getPosts'            => ['enable' => true,    'process' => 0],
                'wp.newTerm'            => ['enable' => false,    'process' => 0],
                'wp.editTerm'            => ['enable' => false,    'process' => 0],
                'wp.deleteTerm'            => ['enable' => false,    'process' => 0],
                'wp.getTerm'            => ['enable' => true,    'process' => 0],
                'wp.getTerms'            => ['enable' => true,    'process' => 0],
                'wp.getTaxonomy'        => ['enable' => true,    'process' => 0],
                'wp.getTaxonomies'        => ['enable' => true,    'process' => 0],
                'wp.getUser'            => ['enable' => false,    'process' => 0],
                'wp.getUsers'            => ['enable' => false,    'process' => 0],
                'wp.getProfile'            => ['enable' => false,    'process' => 0],
                'wp.editProfile'        => ['enable' => false,    'process' => 0],
                'wp.getPage'            => ['enable' => true,    'process' => 0],
                'wp.getPages'            => ['enable' => true,    'process' => 0],
                'wp.newPage'            => ['enable' => false,    'process' => 0],
                'wp.deletePage'            => ['enable' => false,    'process' => 0],
                'wp.editPage'            => ['enable' => false,    'process' => 0],
                'wp.getPageList'        => ['enable' => true,    'process' => 0],
                'wp.getAuthors'            => ['enable' => true,    'process' => 0],
                'wp.getCategories'        => ['enable' => true,    'process' => 0],    // Alias
                'wp.getTags'            => ['enable' => true,    'process' => 0],
                'wp.newCategory'        => ['enable' => false,    'process' => 0],
                'wp.deleteCategory'        => ['enable' => false,    'process' => 0],
                'wp.suggestCategories'        => ['enable' => true,    'process' => 0],
                'wp.uploadFile'            => ['enable' => false,    'process' => 0],    // Alias
                'wp.deleteFile'            => ['enable' => false,    'process' => 0],    // Alias
                'wp.getCommentCount'        => ['enable' => true,    'process' => 0],
                'wp.getPostStatusList'        => ['enable' => false,    'process' => 0],
                'wp.getPageStatusList'        => ['enable' => false,    'process' => 0],
                'wp.getPageTemplates'        => ['enable' => false,    'process' => 0],
                'wp.getOptions'            => ['enable' => false,    'process' => 0],
                'wp.setOptions'            => ['enable' => false,    'process' => 0],
                'wp.getComment'            => ['enable' => true,    'process' => 0],
                'wp.getComments'        => ['enable' => true,    'process' => 0],
                'wp.deleteComment'        => ['enable' => false,    'process' => 0],
                'wp.editComment'        => ['enable' => false,    'process' => 0],
                'wp.newComment'            => ['enable' => false,    'process' => 0],
                'wp.getCommentStatusList'     => ['enable' => false,    'process' => 0],
                'wp.getMediaItem'        => ['enable' => false,    'process' => 0],
                'wp.getMediaLibrary'        => ['enable' => false,    'process' => 0],
                'wp.getPostFormats'         => ['enable' => false,    'process' => 0],
                'wp.getPostType'        => ['enable' => false,    'process' => 0],
                'wp.getPostTypes'        => ['enable' => false,    'process' => 0],
                'wp.getRevisions'        => ['enable' => false,    'process' => 0],
                'wp.restoreRevision'        => ['enable' => false,    'process' => 0],
 
                // Blogger API
                'blogger.getUsersBlogs'     => ['enable' => true,    'process' => 0],
                'blogger.getUserInfo'         => ['enable' => true,    'process' => 0],
                'blogger.getPost'         => ['enable' => true,    'process' => 0],
                'blogger.getRecentPosts'     => ['enable' => true,    'process' => 0],
                'blogger.newPost'         => ['enable' => true,    'process' => 0],
                'blogger.editPost'         => ['enable' => true,    'process' => 0],
                'blogger.deletePost'         => ['enable' => true,    'process' => 0],
 
                // MetaWeblog API (with MT extensions to structs]
                'metaWeblog.newPost'         => ['enable' => true,    'process' => 0],
                'metaWeblog.editPost'         => ['enable' => true,    'process' => 0],
                'metaWeblog.getPost'         => ['enable' => true,    'process' => 0],
                'metaWeblog.getRecentPosts'     => ['enable' => true,    'process' => 0],
                'metaWeblog.getCategories'     => ['enable' => true,    'process' => 0],
                'metaWeblog.newMediaObject'     => ['enable' => true,    'process' => 0],
 
                // MetaWeblog API aliases for Blogger API
                // see http://www.xmlrpc.com/stories/storyReader$2460
                'metaWeblog.deletePost'     => ['enable' => true,    'process' => 0],
                'metaWeblog.getUsersBlogs'     => ['enable' => true,    'process' => 0],
 
                // MovableType API
                'mt.getCategoryList'         => ['enable' => true,    'process' => 0],
                'mt.getRecentPostTitles'     => ['enable' => true,    'process' => 0],
                'mt.getPostCategories'         => ['enable' => true,    'process' => 0],
                'mt.setPostCategories'         => ['enable' => true,    'process' => 0],
                'mt.supportedMethods'         => ['enable' => true,    'process' => 0],
                'mt.supportedTextFilters'     => ['enable' => true,    'process' => 0],
                'mt.getTrackbackPings'         => ['enable' => true,    'process' => 0],
                'mt.publishPost'         => ['enable' => true,    'process' => 0],
 
                // PingBack
                'pingback.ping'             => ['enable' => true,    'process' => 0],
                'pingback.extensions.getPingbacks'     => ['enable' => false,    'process' => 0]
            ]
        ]
    ];
 
    public function __construct() {
        if ($this->config['disable-rpc'] === true) {
            add_filter('option_enable_xmlrpc', false);
        } else {
            if ($this->config['remove-pingback-header'] === true) {
                add_filter('wp_headers', [$this, 'wp_headers']);
            }
            if ($this->config['filter']['enable'] === true) {
                add_filter('xmlrpc_methods', [$this, 'xmlrpc_methods'], 3);
            }
        }
    }
 
    public function wp_headers($headers) {
        unset($headers['X-Pingback']);
 
        return $headers;
    }
 
    public function xmlrpc_methods($methods) {
        if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1') {
            return $methods;
        }
 
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            if (isset($_SERVER['POST']) === false) {
                return [];
            }
 
            foreach ($this->config['filter']['methods'] as $method_name => $method_option) {
                $postData = $postData = $_SERVER['POST'];
 
                if (strpos($postData, $method_name) !== false) {
                    if ($method_option['enable'] === false) {
                        switch ($this->config['filter']['process']) {
                            case 0:
                                unset($methods[$method_name]);
                                break 2;
                            case 1:
                                http_response_code(403);
                                if ($this->config['filter']['message']['enable'] === true) {
                                    header('Content-type: '.$this->config['filter']['message']['content-mime']);
                                    echo $this->config['filter']['message']['message'];
                                }
                                exit;
                                break 2;
                            case 2:
                                http_response_code($this->config['filter']['message']['status']);
                                if ($this->config['filter']['message']['enable'] === true) {
                                    header('Content-type: '.$this->config['filter']['message']['content-mime']);
                                    echo $this->config['filter']['message']['message'];
                                }
                                exit;
                                break 2;
                        }
                        break 1;
                    }
                }
            }
        }
 
        return $methods;
    }
}
 
new kerberos_xmlrpc_filter();
 
?>

コードは凄く簡単。
前処理はイロイロあるけど、add_filterでxmlrpc_methodsにフィルターをかける。
XMLRPCにPOSTが飛んでくると$methodsにWordPressで有効になっているメソッド一覧が入っているので、許可しない場合は$methodsから対象のメソッドを削除すればOK。
削除するだけなら処理はWordPressに引き継がれ有効なメソッドではないとのメッセージが表示される仕組み。

このコードは設定でメソッド毎に許可/却下をするだけなので、改造すればイロイロできます。
うちの場合は、公開ブラックリストの照会やデータベースへの記録・過去のデータベースからスパムかどうかの判定などをしています。

設定のdisable-rpcはXMLRPC自体を無効化してしまうので、pingback等を受け取れなくなるので注意です。

XML-RPCへのアクセスが激化

ここ2日ほどでXML-RPCの脆弱性を狙ったアクセスが急上昇しました。
WordPress - XML-RPC ログ
ログの殆どがXMLRPC関連で2日で600件前後ありました。

アクセスしてくるときの一部のデータはこんな感じ

[AuthDigestEnableQueryStringHack] => On
[CONTENT_LENGTH] => 246
[CONTENT_TYPE] => text/xml
[GATEWAY_INTERFACE] => CGI/1.1
[HTTP_HOST] => blog.wolfs.jp
[HTTP_USER_AGENT] => Mozilla/4.0 (compatible: MSIE 7.0; Windows NT 6.0)
[PHP_SELF] => /xmlrpc.php
[QUERY_STRING] =>
[REMOTE_ADDR] => 5.39.85.104
[REMOTE_PORT] => 55470
[REQUEST_METHOD] => POST
[REQUEST_TIME] => 1411214404
[REQUEST_TIME_FLOAT] => 1411214404.624
[REQUEST_URI] => /xmlrpc.php
[SCRIPT_NAME] => /xmlrpc.php
[SERVER_ADDR] => 180.147.107.163
[SERVER_PORT] => 80
[SERVER_PROTOCOL] => HTTP/1.0
[dont-vary] => 1
[no-gzip] => 1
[POST] => ...

 
送られてくるXMLも変わらずこんなかんじ。

これはpingback
<?xmlversion="1.0"?>
<methodcall>
    <methodname>pingback.ping</methodname>
    <params>
        <param /><value><string>http://kolgeda.com/select_.html</string></value>
        <param /><value><string>http://blog.wolfs.jp/</string></value>
    </params>
</methodcall>
 
これはgetUsersBlogs
<?xml version="1.0" encoding="iso-8859-1"?>
<methodcall>
    <methodname>wp.getUsersBlogs</methodname>
    <params>
        <param /><value><string>admin</string></value>
        <param /><value><string>test</string></value>
    </params>
</methodcall>

ちょっと前から思っていたんだけど、この系統のアクセスをしてくるユーザーのユーザーエージェントは決まってMozilla/4.0 (compatible: MSIE 7.0; Windows NT 6.0)なんですよね。
Windows VistaのInternet Explorer 7。

こんな綺麗なユーザーエージェントはそうないので、IPアドレスで弾くのが面倒になったらこれで弾いても良さそう。
送信元の殆どがフランスってのも気になる所。
続きを読む

XML-RPC アタック

WordPressのXML-RPCを使ったアタックやスパムが少しだけあったのですが、それがここ最近酷くなってきました。
ログを見てみると・・・あるわあるわ・・
WordPress - XML-RPC ログ

XML-RPC等、スパム系の物は詳細ログを取っているのでそれを表示してみる。
WordPress - XML-RPC 詳細情報
うんうん、やっぱりPingbackを使って他サイトへ負荷をかけているみたい。
だけれどうちは安心で、WordPressにログインしたIPアドレスからでないとpingback.pingを使えないように改造してあるのです。
 
そのログの中のPOSTにはこんなのがありました。

<?xml version="1.0"?>
<methodCall>
    <methodName>pingback.ping</methodName>
    <params>
        <param><value><string>http://ru.pmvf.net/calculator</string></value></param>
        <param><value><string>http://blog.wolfs.jp/</string></value></param>
    </params>
</methodCall>

大抵はこのXMLで、これはPingbackを送信する物。
 
稀にこんなのも混ざってたりします。

<?xml version="1.0"?>
<methodCall>
    <methodName>metaWeblog.getUsersBlogs</methodName>
    <params>
        <value><string>1</string></value><value><string>admin</string></value><value><string>admin</string></value>
    </params>
</methodCall>

これはブログを情報を表示させるXMLで、paramsで指定されているのはユーザー名とパスワード。
うちのブログはXML-RPCからログインできるユーザーを別に用意してあるので大丈夫。
仮にユーザー名とパスワードが判っても、WordPressメインユーザーでログインしたIPアドレスと同じでないとログインできないので問題ナシ。
 
それでもトライされるのは気持ちいいもんじゃないし、そんな奴らにリソースを使うのはイヤなのでIP層でパケットをドロップしてやります。