声明:本文章敏感部分已做了相应的处理,不代表任何利益和立场,且仅限于安全研究与教学使用,读者使用本文章方法所造成的所有后果,由用户承担全部法律及连带责任!作者不承担任何法律及连带责任。
背景介绍
某次bar冲浪时,看到一款路由产品,习惯性登录,提示密码错误,bing搜一下找到默认的账号、密码,登陆之,直接进后台——嗯,后台功能挺丰富。
萌生了爆破的念头,到fofa上搜一下,居然发现好几万台这样的设备,好的,这就写脚本。在分析登录请求包的时候,发现它前端的参数居然是加密的,眼看应该是不能够通过重放来登录了,但在js文件里又发现了玄机...
正常情况下的前端加密过程
正常情况下,有效的前端加密逻辑是:首先生成随机的密钥key,然后使用key对原文进行加密,再将密钥和密文都发给服务器,服务器要么利用key和密文解密得到原文(如aes加密),要么直接比较密文(如md5哈希),其中key可能是随机的,也可能是固定的。下面整理一种aes前端加密配合后端解密的常见流程
【获取随机key】 ->
【用key进行加密 encrypted_text = encrypt(key, password) 】 ->
【将key和encrypted_text都发送给服务端】 ->
【服务端解密、验证、返回验证结果】
我将这个过程抽象,得出以下三个结论:
-
开发者之所以进行前端加密,是因为 不希望攻击者知晓原文 (例:防止MitM中间人攻击、burp明文传输)
-
原文 <==( 密钥+加密算法 )==>密文
-
加密算法 绝大部分 是写在js里的,而密钥 常常是可控 的,甚至有些被硬编码了,如图所示(图片参考地址ref-1)
当然了,你可能会说,RSA算法是例外呀,加密和解密的key可以不是同一个,这样即使加密的key泄露了也无法解密。
没错,但如果不考虑这种特殊情况,我们就可以说: 在密钥key已知的情况下,要想使攻击者猜不出原文,就要使加密算法足够复杂,增加攻击者分析算法的成本,进而提高攻击者自己加密的难度。 遗憾的是,加密、解密常常已被封装成了单个函数,而算法本身更是简单到离谱,这就使得前端js加密形同虚设。。。
好,说了这么多废话,咱们言归正传——分析分析这个前台登录界面的加密算法
登录处账号密码加密算 法的分析
当账号为默认账号 wwws
、密码为默认密码 admin
时,登录处的POST包如下
POST /wenwang/login.cgi HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4086.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 98
Connection: close
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
user=7WJCDjFKjXyuyq&password=6mwHEERAeYcbogp&ww_loginpage=1&csrfprotect=&Submit=%E7%99%BB%E5%BD%95
在F12 debugger里发现加密函数 formFun()
<li>
<label>账号:</label>
<input name="user1" id="user1" type="text" class="loginuser" value="" />
</li>
<li>
<label>密码:</label>
<input name="password1" id="password1" type="password" class="loginpwd" value="" AUTOCOMPLETE="off"/>
</li>
<form name="login_form" action="login.cgi" method="post" onSubmit="return formFun()"><!--加密后的在这里-->
跟进加密函数,
function formFun(){
var user = document.getElementById("user1").value; //'wwws'
var paswd = document.getElementById("password1").value; //'admin'
...
var t_user = randomString(10);//10位的随机字符串
var t_paswd = randomString(10);
var i = 0;
for(i=0;i<user.length;i++) //用户名加密 'wwws' -> 'yuyq'
{
if(i%2 == 0)
t_user += String.fromCharCode(user.charCodeAt(i)+2);
else
t_user += String.fromCharCode(user.charCodeAt(i)-2);
}
for(i=0;i<paswd.length;i++) //密码加密 'admin' -> 'cbogp'
{
if(i%2 == 0)
t_paswd += String.fromCharCode(paswd.charCodeAt(i)+2);
else
t_paswd += String.fromCharCode(paswd.charCodeAt(i)-2);
}
document.getElementById("user").value=t_user;
document.getElementById("password").value=t_paswd;
return true;
}
里面用到的几个函数,用法都比较简单,我的理解如下:
-
randomString()
,可返回指定长度的随机字符串 -
charCodeAt()
,可返回指定位置的字符的 Unicode 编码。字符串中第一个字符的位置为 0, 第二个字符位置为 1,以此类推,如'w'.charCodeAt()=119
-
String.fromCharCode()
,可根据Unicode值返回一个字符串。如String.fromCharCode(119)="w"
间隔移位算法
其实,加密过程中的核心算法,就是下面的这段函数,我们来看看
for(i=0;i<user.length;i++) //用户名加密 'wwws' -> 'yuyq'
{
if(i%2 == 0)
t_user += String.fromCharCode(user.charCodeAt(i)+2);//奇数位上的字符串向上偏移2位,即unicode值+2
else
t_user += String.fromCharCode(user.charCodeAt(i)-2);//偶数位上的字符串向下偏移2位,即unicode值-2
}
}
举个例子:原字符串为 wwws
, 经过上面的算法处理过后,就成了 yuyq
。
我给它取名叫“间隔移位算法”,示意图如下,最左边一栏是Unicode的顺序,绿色是原文,蓝色是算法处理得到的“密文”;。
理清了核心算法,我们再回过去看看漏了些什么: t_user
和 t_paswd
都是有初始值的—— 长度为10的随机字符串!
var t_user = randomString(10);//10位的随机字符串
var t_paswd = randomString(10);
所谓的间隔移位算法,将原文"加密"后,是拼接在初始值后面的。同样用原字符串为 wwws
来举例说明一下:
wwws -> yuyq -> AAAAAAAAAAyuyq
, 这10个A就代表长度为10的随机字符串。
实际上,分析到这里,它登录处在后端解密的整个逻辑,就呼之欲出了。解密时:从第11位开始,截取字符串,得到间隔移位算法处理过的字符串,即上图中“密文”,再根据奇偶位移位的不同规则,恢复成原文即可 AAAAAAAAAAyuyq -> yuyq -> wwws
结论
对于每一次登陆时POST的值来说,前10位是没有实际作用的,我们可以随意填充。从第11位开始的值,才真正被服务器接收,移位解密后,用于验证账号和密码是否正确。
因此,对于默认口令的检测,我们只需要将移位加密后的账号、密码,拼接到10个任意字符串之后即可。(其实, 验证默认口令时,重放数据包就可以了 XD )
无需POST包的登录
看到登录成功后它返回的Cookie值,我端起桌上的菊花茶,陷入了沉思。。。
Set-Cookie: gw_userid=AAAAAAAAAAyuyq,gw_passwd=3DDE690BBFDBCF8E3E258CBC40C0B9BF;
不妨来分析一下这段cookie,先看 gw_userid=AAAAAAAAAAyuyq
,这不就是刚刚登陆时POST包里的 user
参数吗。那 gw_passwd
后面的32位字符串呢,怕不是密码哈希,尝试到各大平台上解密,无果。不过不打紧,只要登录密码是admin,即便是在不同的设备上,gw_passwd也是这个值(加密规则相同)。
因此,为了验证是否可以用默认密码登录,连POST包都不用发,直接带着这个cookie访问主页,观察会是否302跳转到登录界面即可。若跳转即证明cookie无效,使用的密码并非默认密码,否则密码就是默认密码。
至此,编写批量验证脚本的思路已经完全分析清楚了。
总结
由于该后台登录处采用固定方法“加密”,没有起到真正的加密效果,导致可用重放的方法来尝试登录,甚至在完全弄清楚算法后,可以自行加密、进行爆破。
reference
-
前端JS加密那些事 - Gcker - 博客园https://www.cnblogs.com/Gcker/p/12222761.html
-
RSA算法原理(公钥/私钥)https://blog.csdn.net/qq_27489007/article/details/100597938#3-rsa解密