SECCON2017国内決勝大会の振り返り

2018.02.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、臼田です。

今回は個人的に参加してきたSECCON2017 国内決勝大会の振り返りとしてブログを残したいと思います。

完全に個人向け・チーム向け・参加した人向け程度の内容なので細かいところは説明しません。

サマリ

今回ずっと個人的に参加していたチーム「よかろうもん」で初めてSECCONの決勝大会に出ることができました。

SECCON2014から初めて、本戦(国際大会)ではないにしろ決勝に出れたことはとても嬉しかったです。

結果としては24チーム中12位という悔しい結果でした。特に11位以上と1問(100pt)以上差が開いていることから、まだまだ精進が必要だと感じました。

当日の動き

序盤は自分が得意なジャンル(Crypto)が無いことに落胆しつつ、梅田のDefenceポイントを稼ぐためのスクリプトを実装しました。

最終的なコードは下記の通り。かなり生々しいのであしからず。

import requests
import re
import json
import time
import datetime

target_base = 'http://umeda.koth.seccon/photos/'
mosted_num = '28847'
target = target_base + '28847'
mosted = 'http://umeda.koth.seccon/most-liked'

flag_file = 'flagword.txt'
flagword = "468339b255e76b47fe3f2b2f307d0a47"

# name="csrf_name" value="csrf5a879d83a15e9">
re_csrf_name = re.compile(r'.*name="csrf_name" value="([a-z0-9]*)".*name="csrf_name" value="([a-z0-9]*)".*')
# name="csrf_value" value="4180b70b4534af3be3454c7e87d2d0cf">
re_csrf_value = re.compile(r'.*name="csrf_value" value="([a-f0-9]*)".*name="csrf_value" value="([a-f0-9]*)".*')
# <img class="photo-individual" src="/photos/200/raw">
re_mosted = re.compile(r'.*<img class="photo-individual" src="/photos/([0-9]+)/raw">.*')

s = requests.Session()
cookies = dict(PHPSESSID='eece7c193b808e6d8b49e4a68e050678')

count = 0
while 1:
	if count == 0:
		try:
			# get mosted like
			r3 = s.get(mosted, cookies=cookies)
			o = re.match(re_mosted, r3.text)
			mosted_num = o.group(1)
			target = target_base + mosted_num
			print 'change: ' + target
		except Exception as e:
			print('err: check')
			# read flagword
		with open(flag_file, 'r') as f:
			for line in f:
				flagword = line.strip()
				print 'flagword: ' + flagword

	try:
		# get csrf token
		r = s.get(target, cookies=cookies)
		# print(r.text)

		m = re.match(re_csrf_name, r.text)
		csrf_name = m.group(2)
		n = re.match(re_csrf_value, r.text)
		csrf_value = n.group(2)
		# print csrf_name
		# print csrf_value

		# send
		payload = {'csrf_name': csrf_name, 'csrf_value': csrf_value, 'content': flagword}
		r2 = s.post(target + '/comment', data=payload, cookies=cookies)

		print r2.status_code, ':', mosted_num, ':', datetime.datetime.now().isoformat(), ':', flagword
		# time.sleep(1)
		count += 1
	except Exception as e:
		print('err: flag')
		time.sleep(1)

	if count > 60: count = 0

急いで実装しているので、いろいろひどい…

ただ、欲しい部分をコメントに書いて、見ながら正規表現書くのはやりやすいと思いました。

序盤はmosted_likeのページが固定だったが、中盤から60回に1回ぐらい見に行くようにしました。

Defenceのflagwordも別のスクリプトで取得したものをflagword.txtに保存しておいてそれを読み込むようにしました。これは余計なクエリを挟んでフラグを逃さないため。

1回のクエリあたり、平均的に1.5秒くらいかかってたからCSRFトークン取得とsubmitで3秒位、フラグを取れるかどうかは結構ギリギリなので、他のサーバ用としても使えるという意味でDefenceフラグの外出しは正しいと思います。

というか、今思ったらsubmitしたページのCSRFトークン使い回せばよかった…

途中でmosted_likeが死ぬことがあったので(PHPがエラーを起こす内容をsubmitしたか、photos/9が消えていたことからmosted_likeが削除されたことによるバグだと考えられた)、手離れがいいように全部tryに突っ込みました。後半は殆ど面倒を見ていなかったので良かったと思う。突貫工事のスクリプトほどtryが役立つところはないですね。

終盤は幕張の2つ目を探していたけど見つけることができませんでした。まさかCertの中にあるなんて…orz

後から考える戦略

上記を実装した後に少し迷ったけど、もう少し他チームを落とすためのmosted_likeの取り合いを頑張っても良かった気がしました。

結局手を付けられなかったが、新規アカウントの作成と、任意のページのlike自体は両方共簡単に実装できそうだったと思います。後半はmosted_likeはほとんど200のままだったので、余地はあった気がします。

梅田のDefenceポイントは5分おきに3ptsずつ入っていたように見受けられました。殆どのチームが梅田にフラグを立てていたように思えるので最大で3 * 20で60ptsの山分け、15チームだったとしても45pts と割といいポイント。

各チームのmosted_likeのチェック頻度によるし、後半には他チームも同じような事をしてもおかしくない(というか大会中に実際に行われていなかった事が不思議ですが)、少ない回数でも大量にポイントを獲得する機会が増えれば、1問解くよりも効率は良かったように思えました。

まあ、結果的にAtackを取れなかったのでよりそう思ったわけですが。

まとめ

Attackを頑張ること、スクリプトの実装速度をあげることも必要だと感じましたが、Defenceを増やしに行く戦略ももう少し頑張りたいと思いました。

次回は入賞したい!