用户
搜索
  • TA的每日心情
    无聊
    2021-3-16 19:57
  • 签到天数: 8 天

    连续签到: 1 天

    [LV.3]经常看看I

    i春秋作家

    Rank: 7Rank: 7Rank: 7

    4

    主题

    16

    帖子

    279

    魔法币
    收听
    0
    粉丝
    1
    注册时间
    2019-10-8

    i春秋签约作者

    发表于 2021-12-18 15:32:43 22548
    本帖最后由 fatmo 于 2021-12-24 20:16 编辑

    本文原创作者fatmo,本文属i春秋原创奖励计划,未经许可禁止转载。
    往期回顾:
    1. SRC信息收集学习与自动化(一):开发基础知识巩固
    2. SRC信息收集学习与自动化(二):CDN探测

    0x01 前言

    注:本文是基于项目https://github.com/proudwind/phpinfo_scanner的修改和补充

    phpinfo页面是渗透测试中时常遇到的页面,它给我们提供了不少有价值的信息,其中包括:

    1. 网站绝对路径
    2. 网站真实IP地址
    3. 网站php版本
    4. …………

    phpinfo01.png

    phpinfo02.png

    利用脚本收集phpinfo中的信息,可以减少我们重复性的工作,避免遗漏,更重要的是,可以与项目其他模块进行配合,实现其他模块的功能,如给CDN绕过提供通过phpinfo获取真实ip的功能。

    0x02 可利用信息分析

    00 环境复现

    01 环境复现较为简单,本文使用phpstudy搭建wamp环境,然后点击启动开启环境

    phpinfo03.png

    02 然后进入网站根目录,在根目录下新建文件phpinfo.php,加入如下代码:

    <?php
        phpinfo();

    03 浏览器访问http://127.0.0.1/phpinfo.php,看到如下界面,即复现成功

    phpinfo04.png

    01 网站绝对路径

    在渗透测试中,有多处需要知道网站的绝对路径,如通过sql注入写入一句话木马时,phpinfo页面有多处泄露了网站的绝对路径,如:

    1. PHP Variables > _SERVER["SCRIPT_FILENAME"]
    2. PHP Variables > _SERVER["DOCUMENT_ROOT"]

    phpinfo05.png

    02 网站真实ip地址

    我们有时会遇见网站使用了CDN加速的情况,导致无法直接获得目标主机的真实ip地址,那么如果目标存在phpinfo页面,我们可以直接通过phpinfo获得目标主机ip。

    路径:PHP Variables > _SERVER["SERVER_NAME"]

    phpinfo06.png

    03 FPM未授权访问漏洞

    PHP-FPM存在未授权访问漏洞,我们可以通过phpinfo查看网站是否使用了PHP-FPM,若使用且目标主机开启9000端口则可尝试利用。

    路径:Server api

    phpinfo08.png

    漏洞原理请看:https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

    漏洞复现:

    01 vulhub有现成的环境可以使用:https://github.com/vulhub/vulhub

    02 执行以下指令创建环境:

    # 下载项目
    wget https://github.com/vulhub/vulhub/archive/master.zip -O vulhub-master.zip
    unzip vulhub-master.zip
    cd vulhub-master
    
    # 进入某一个漏洞/环境的目录
    cd php/fpm/
    
    # 自动化编译环境
    docker-compose build
    
    # 启动整个环境
    docker-compose up -d

    03 下载exp:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

    04 尝试执行exp,复现成功

    phpinfo07.png

    04 phar扩展反序列化攻击面

    利用phar协议,可以扩展php反序列化攻击的攻击面,具体见:https://www.jianshu.com/p/0cfc98f72e52

    路径:Registered PHP Streams

    phpinfo09.png

    05 gopher协议、dict协议扩展SSRF攻击面

    在SSRF漏洞中,我们可以通过gopher协议和dict协议来扩展攻击面,具体见:

    gopher协议:https://zhuanlan.zhihu.com/p/112055947

    dict协议:https://www.cnblogs.com/zzjdbk/p/12970919.html

    路径:curl > Protocols

    phpinfo10.png

    06 获取libxml版本判断是否可利用xxe漏洞

    我们知道只有libxml版本<2.9时,xxe可利用,因此我们可以通过phpinfo透露的信息查看libxml的版本。

    xxe详情见:https://security.tencent.com/index.php/blog/msg/69

    路径:libxml > libXML Compiled Version

    phpinfo11.png

    07 上传临时文件并包含可能性探测

    session.upload_progress.enabled=On,利用session.upload_progress上传一个临时文件,该文件里面有我们上传的恶意代码(一般为webshell写入代码),然后包含它,从而执行里面的代码。

    但如果session.upload_progress.cleanup=On,则表示临时文件会很快被清楚,就需要我们不停访问进行竞争上传.

    具体见:https://blog.csdn.net/qq_44657899/article/details/109281343

    路径:

    1. session > session.upload_progress.enabled
    2. session > session.upload_progress.cleanup

    phpinfo12.png

    08 serialize_handler反序列化风险

    serialize_handler的Local Value和Master Value不一致,则可能导致反序列化漏洞。

    详见:https://www.cnblogs.com/vege/p/12575371.html

    路径:session > session.serialize_handler

    补充:

    1. Master Value是PHP.ini文件中的内容。

    2. Local value 是当前目录中的设置,这个值会覆盖Master Value中对应的值

    phpinfo13.png

    09 imagick组件相关漏洞

    可通过phpinfo查看目标有没有使用imagick组件及其版本号,进而发现可以利用的漏洞。

    具体见:https://www.anquanke.com/post/id/83872

    路径:imagick

    10 xdebug rce漏洞

    可通过phpinfo查看目标有没有使用xdebug且xdebug.remote_enable是否为On,进而发现可以利用的漏洞。

    具体见:https://github.com/vulhub/vulhub/tree/master/php/xdebug-rce

    路径:xdebug > xdebug.remote_enable

    11  opcache组件相关漏洞

    可通过phpinfo查看目标有没有使用opcache组件及其版本号,进而发现可以利用的漏洞。

    具体见:https://www.cnblogs.com/xhds/p/13239331.html

    路径:opcache

    12 imap组件相关漏洞

    可通过phpinfo查看目标有没有使用imap组件及其版本号,进而发现可以利用的漏洞。

    具体见:https://github.com/vulhub/vulhub/blob/master/php/CVE-2018-19518/README.md

    路径:imap

    13 disable_functions发现及绕过

    我们可以在disable_functions中找到目标禁用的函数,并通过禁用的函数列表找到绕过方法.

    路径:Core > disable_functions

    phpinfo14.png

    0x03 代码编写

    01 第一步很常规,写一个类,名为PhpinfoCheck,继承自BaseObject,然后完善构造函数

    class PhpInfoCheck(BaseObject):
        def __init__(self):
            BaseObject.__init__(self)
            self.domains = []
            self.queryResult = {}
            self.result = {}
    
            args = self.argparser()
            # 生成主域名列表,待检测域名入队
            target = args.target
            if not os.path.isfile(target):
                # target = 'http://' + target
                self.domains.append(target)
            elif os.path.isfile(target):
                with open(target, 'r+', encoding='utf-8') as f:
                    for domain in f:
                        domain = domain.strip()
                        if not domain.startswith(('http://', 'https://')):
                            self.domains.append(domain)

    02 随后完成协程调用收集方法逻辑,同样很常规

        def startQuery(self):
            try:
                tasks = []
                newLoop = asyncio.new_event_loop()
                asyncio.set_event_loop(newLoop)
                loop = asyncio.get_event_loop()
    
                for domain in self.domains:
                    if os.path.exists(os.path.dirname(os.path.abspath(__file__)) + '/result/' + domain + '/') is False:
                        os.mkdir(os.path.dirname(os.path.abspath(__file__)) + '/result/' + domain + '/')
    
                    tasks.append(asyncio.ensure_future(self.infoCollect(domain)))
    
                loop.run_until_complete(asyncio.wait(tasks))
            except KeyboardInterrupt:
                self.logger.info('[+]Break By User.')
            except CancelledError:
                pass
    
            self.writeResult()

    03 之后便是方法self.infoCollect(),首先需要使用方法self.sendReques()请求目标站点并得到返回包,得到返回包,以返回包和目标域名作为输入调用本脚本最重要的两个方法self.infoCollecter()self.get_parsed_info()

        async def infoCollect(self, domain):
            self.queryResult[domain] = {}
    
            for item in phpinfoList:
                response = await self.sendRequest('http://' + domain + '/' + item)
                if response != False:
                    break
    
            self.infoCollecter(domain, response)
            self.get_parsed_info(domain)

    04 在完成方法self.infoCollecter()之前,我们需要了解一下CSS选择器的使用

    我们在写CSS时,需要定位到我们需要装饰的元素上,这时候就需要使用CSS选择器,比如我们需要定位所有class="ichunqiu"的元素,就需要这样写.ichunqiu,我们要定位所有id=ichunqiu的元素,就需要这样写#ichunqiu

    如果要定位所有的<p>标签,直接使用p即可,有些标签之间存在父子关系,比如我们需要定位所有所有父级是<div> 元素的 <p> 元素,就这样写div > p

    如果要定位<div>标签中的第二个<p>标签,可以这样写div > p:nth-child(2)

    05 我们可以在BeatifulSoup中,利用CSS选择器拿到我们需要的标签,用法如下:

    soup = BeautifulSoup(response, "lxml")
    info = soup.select("body > div ")

    06 我们再看看phpinfo上的html结构

    先取最上面的基本信息部分,它位于body标签中的div标签中第二个或第三个table标签中

    phpinfo15.png

    那么利用我们之前学习的CSS选择器的知识,我们可以用这一段代码取出所有基本信息:

    if len(soup.select("body > div >table:nth-child(2)")) != 0:
        baseInfo = soup.select("body > div >table:nth-child(2)")[0]
    else:
        baseInfo = soup.select("body > div >table:nth-child(3)")[0]

    然后再依次取出其中的tr标签,存入结果字典中

    for tr in baseInfo.find_all("tr"):
        key = tr.select("td.e")[0].string.strip()
        value = tr.select("td.v")[0].string.strip()
        self.queryResult[domain]["BaseInfo"][key] = value

    基本信息之后就是配置信息,观察结构,它都是一个h2标签后跟着一个或多个table

    phpinfo16.png

    我们可以这样把所有的h2标签取出来:

    for h2 in soup.find_all("h2"):
        moduleName = h2.string.strip()
        self.queryResult[domain][moduleName] = {}

    然后通过next_siblings属性一致往下抓取信息,直到next_siblings不是table为止

    for h2 in soup.find_all("h2"):
        moduleName = h2.string.strip()
        self.queryResult[domain][moduleName] = {}
        # 每一个配置模块是从h2标题开始的,向下寻找所有的table标签
        # 有一个特殊情况PHP Credits,它在h1标签中,其内容是php及其sapi、module等的作者,对脚本功能没有意义,所以不解析
        for sibling in h2.next_siblings:
            # 使用next_siblings会匹配到许多\n \t等,需特殊处理,官方文档明确提到
            if sibling.name != "table" and type(sibling) != element.NavigableString and sibling.name != "br":
                break
            if sibling.name == "table":
                for tr in sibling.find_all("tr"):
                    keyElements = tr.select("td.e")
                    if len(keyElements) == 0:
                        continue
                    key = keyElements[0].string.strip()
    
                    valueElements = tr.select("td.v")
                    if len(valueElements) == 0:
                        value = ''
                    elif len(valueElements) == 2:
                        # 有些配置的value分为Local Value和Master Value
                        # local value是当前目录的设置,会受.htaccess、.user.ini、代码中ini_set()等的影响
                        # master value是php.ini中的值
                        value = [valueElements[0].string.strip(), valueElements[1].string.strip()]
                    else:
                        value = "no value" if valueElements[0].string == None else valueElements[0].string.strip()
                    self.queryResult[domain][moduleName][key] = value

    最后在Windows和Linux系统中,部分配置项存在差异,需要我们消除差异

    # windos _SERVER["xx"]
    # linux $_SERVER['xx']
    # 消除这种差异
    php_var_dict = {}
    if list(self.queryResult[domain]["PHP Variables"].keys())[0][0] == "_":
        for key in self.queryResult[domain]["PHP Variables"].keys():
            new_key = "$" + key.replace('"', "'")
            php_var_dict[new_key] = self.queryResult[domain]["PHP Variables"][key]
        self.queryResult[domain]["PHP Variables"] = php_var_dict

    self.infoCollecter()就完成了,代码全貌如下:

    def infoCollecter(self, domain, response):
        self.queryResult[domain]["BaseInfo"] = {}
        soup = BeautifulSoup(response, "lxml")
        if len(soup.select("body > div >table:nth-child(2)")) != 0:
            baseInfo = soup.select("body > div >table:nth-child(2)")[0]
        else:
            baseInfo = soup.select("body > div >table:nth-child(3)")[0]
    
        for tr in baseInfo.find_all("tr"):
            key = tr.select("td.e")[0].string.strip()
            value = tr.select("td.v")[0].string.strip()
            self.queryResult[domain]["BaseInfo"][key] = value
    
        for h2 in soup.find_all("h2"):
            moduleName = h2.string.strip()
            self.queryResult[domain][moduleName] = {}
            # 每一个配置模块是从h2标题开始的,向下寻找所有的table标签
            # 有一个特殊情况PHP Credits,它在h1标签中,其内容是php及其sapi、module等的作者,对脚本功能没有意义,所以不解析
            for sibling in h2.next_siblings:
                # 使用next_siblings会匹配到许多\n \t等,需特殊处理,官方文档明确提到
                if sibling.name != "table" and type(sibling) != element.NavigableString and sibling.name != "br":
                    break
                if sibling.name == "table":
                    for tr in sibling.find_all("tr"):
                        keyElements = tr.select("td.e")
                        if len(keyElements) == 0:
                            continue
                        key = keyElements[0].string.strip()
    
                        valueElements = tr.select("td.v")
                        if len(valueElements) == 0:
                            value = ''
                        elif len(valueElements) == 2:
                            # 有些配置的value分为Local Value和Master Value
                            # local value是当前目录的设置,会受.htaccess、.user.ini、代码中ini_set()等的影响
                            # master value是php.ini中的值
                            value = [valueElements[0].string.strip(), valueElements[1].string.strip()]
                        else:
                            value = "no value" if valueElements[0].string == None else valueElements[0].string.strip()
                        self.queryResult[domain][moduleName][key] = value
    
        # windos _SERVER["xx"]
        # linux $_SERVER['xx']
        # 消除这种差异
        php_var_dict = {}
        if list(self.queryResult[domain]["PHP Variables"].keys())[0][0] == "_":
            for key in self.queryResult[domain]["PHP Variables"].keys():
                new_key = "$" + key.replace('"', "'")
                php_var_dict[new_key] = self.queryResult[domain]["PHP Variables"][key]
            self.queryResult[domain]["PHP Variables"] = php_var_dict

    07 我们去到所有的信息后,就可以调用方法self.get_parsed_info()来分析是否存在风险了,风险信息上文已经描述过,方法具体代码如下:

        # 解析获取到的信息,如bypass_disable_function、php版本特性等
        def get_parsed_info(self, domain):
            self.result[domain] = []
            # php version
            suggestion = self.get_version_feature(self.queryResult[domain]["Core"]["PHP Version"])
            if suggestion:
                self.result[domain].append([suggestion])
            # sapi
            sapi = self.queryResult[domain]["BaseInfo"]["Server API"]
            if "FPM" in sapi:
                self.result[domain].append(["SAPI为fpm,可能存在未授权访问漏洞"])
            # phar
            if "phar" in self.queryResult[domain]["BaseInfo"]["Registered PHP Streams"]:
                self.result[domain].append(["支持phar协议,可扩展反序列化攻击面"])
            # ssrf curl php_wrapper
            protocols = ["gopher", "dict"]
            available_protocols = []
            if "curl" in self.queryResult[domain]:
                for protocol in protocols:
                    if protocol in self.queryResult[domain]["curl"]["Protocols"]:
                        available_protocols.append(protocol)
                self.result[domain].append(["libcurl支持%s协议" % (", ".join(available_protocols))])
            # libxml版本
            if "libxml" in self.queryResult[domain] and self.queryResult[domain]["libxml"]["libXML Compiled Version"] < "2.9":
                self.result[domain].append(["libxml版本 < 2.9 xxe可利用"])
            # session upload progress
            if self.queryResult[domain]["session"]["session.upload_progress.enabled"][0] == "On":
                suggestion = "可利用session.upload_progress上传临时文件然后包含"
                if self.queryResult[domain]["session"]["session.upload_progress.cleanup"][0] == "On":
                    suggestion += "\n临时文件会立刻删除,需用条件竞争getshell"
                self.result[domain].append([suggestion])
            # session ser handler
            if self.queryResult[domain]["session"]["session.serialize_handler"][0] != \
                    self.queryResult[domain]["session"]["session.serialize_handler"][1]:
                self.result[domain].append(["ser handler不一致,存在反序列化风险"])
            # imagick
            if "imagick" in self.queryResult[domain]:
                self.result[domain].append(["可利用imagick相关漏洞"])
            # xdebug
            if "xdebug" in self.queryResult[domain] and self.queryResult[domain]["xdebug"]["xdebug.remote_connect_back"][0] == "On" and \
                    self.queryResult[domain]["xdebug"]["xdebug.remote_enable"][0] == "On":
                self.result[domain].append(["存在xdebug rce https://github.com/vulhub/vulhub/tree/master/php/xdebug-rce\nxdebug idekey: " +
                               self.queryResult[domain]["xdebug"]["xdebug.idekey"][0]])
            # opcache
            if "opcache" in self.queryResult[domain]:
                self.result[domain].append(["可上传opcache覆盖源文件"])
            # imap
            if "imap" in self.queryResult[domain]:
                self.result[domain].append(["可能存在imap rce https://github.com/vulhub/vulhub/blob/master/php/CVE-2018-19518/README.md"])
            # disable function
            if self.queryResult[domain]["Core"]["disable_functions"][0] != "no value":
                self.result[domain].append([self.bypass_disable_function(self.queryResult[domain]["Core"]["disable_functions"][0], self.queryResult[domain])])

    08 完善方法self.bypass_disable_function

    # 如果存在disable_function,寻找可能的bypass
    def bypass_disable_function(self, disable_func, phpinfo_dict):
        disable_func = disable_func.split(",")
        suggestion = ""
        bypass_func = []
    
        if "dl" not in disable_func and phpinfo_dict["Core"]["enable_dl"] == "On":
            bypass_func.append("dl")
        if "pcntl_exec" not in disable_func and "--enable-pcntl" in phpinfo_dict["BaseInfo"]["Configure Command"]:
            bypass_func.append("pcntl_exec")
        common_funcs = ['exec', 'system', 'passthru', 'popen', 'proc_open', 'shell_exec']
        for func in common_funcs:
            if func not in disable_func:
                bypass_func.append(func)
        suggestion += "可用函数:" + ", ".join(bypass_func) + "\n"
    
        if "Linux" in phpinfo_dict["BaseInfo"][
            "System"] and "putenv" not in disable_func and "mail" not in disable_func:
            suggestion += "使用LD_PRELOAD https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD\n"
        if "imap" in phpinfo_dict:
            suggestion += "使用imap https://github.com/vulhub/vulhub/blob/master/php/CVE-2018-19518/README.md\n"
        if "imagemagick" in phpinfo_dict:
            suggestion += "使用 ImageMagick\n"
        suggestion += "disable function bypass合集 https://github.com/l3m0n/Bypass_Disable_functions_Shell"
        return suggestion

    09 大功告成了!源码已在:GitHub:https://github.com/fatmo666/InfoScripts/blob/master/PhpInfoCheck.py

    发表于 2021-12-19 10:51:13
    使用道具 举报 回复
    发表于 2022-1-14 14:26:22
    表哥加局长!来领奖励了!!!
    对论坛发展有任何想法
    欢迎+微信/QQ:826177911来搞事!
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册