项目介绍
360路由器目前没有无损刷机的办法,获取ssh终端权限较为困难,如想获取路由器状态信息以及连接设备信息就比较困难,但可以通过采用逆向破解网页登陆的形式进行后台的登录进行信息获取。
项目地址
https://github.com/hamster1963/360-router-data-retriever
实现效果
采用代码进行360后台的登录,进行路由器信息的获取。

登陆过程分析
抓包
直接采用Safari的开发者工具进行抓包。
在输入框中输入管理员密码进行登录。


可以看到页面首先请求了get_rand_key.cgi(CGI是通用网关接口,是一种比较传统的动态网页的实现方式),通过Get方法获取到了一串随机的字符串。

在获取完成后进行一些暂时未知的处理后请求了web_login.cgi,请求数据中包括了管理员用户名与密码,可以发现pass密码中的值并不是我们所填入的密码值,首先猜测可能为md5或aes加密后的字符串。

查看登陆接口在登陆成功后的返还与Header。接口返还了登陆状态码以及Token-ID字段数据,同时在返回的Header中可以看到有Set-Cookie字段,猜测在后续的数据接口中应该会在请求头中携带token以及cookie进行权限验证。


查看后续请求头,可以观察到在Cookie中携带了先前返回的信息,包含Set-Cookie以及token信息。

目前为止,可以对登陆过程进行一个小总结。
输入账号密码→点击登录→获取随机字符串→进行加密→请求登录接口→获取Cookie以及token
目前主要难点为如何进行加密的逆向破解。
逆向
首先尝试md5加密,与目标密码不一致。
其次尝试简单粗暴的方式,查源码。查看js代码后,发现js代码进过webpack混淆打包,从函数名以及变量名中很难找出加密的代码块,因此使用关键词进行搜索。

果然还是简单粗暴比较有用,在搜索到AES关键词后,可以从代码中发现密码加密的关键逻辑

switch ((t.prev = t.next)) {
  case 0:
    return (
      (t.next = 2),
      c(0).then(function (t) {
        r = n || t
        var o = a.enc.Hex.parse(r.rand_key),
          u = a.enc.Latin1.parse('360luyou@install'),
          c = a.AES.encrypt(e, o, {
            iv: u,
            mode: a.mode.CBC,
            padding: a.pad.Pkcs7,
          })
        i = c.ciphertext.toString()
      })
    )
  case 2:
    return t.abrupt('return', r.key_index + i)
  case 3:
  case 'end':
    return t.stop()
}switch ((t.prev = t.next)) {
  case 0:
    if (((r = e.substr(32, e.length - 32)), (i = {}), !n)) {
      t.next = 6
      break
    }
    ;(i.rand_key = n), (t.next = 9)
    break
  case 6:
    return (t.next = 8), c(0, e, !0)
  case 8:
    i = t.sent
  case 9:
    if (i.rand_key) {
      t.next = 11
      break
    }
    return t.abrupt('return', e)
  case 11:
    return (
      (u = a.enc.Hex.parse(i.rand_key)),
      (s = a.enc.Latin1.parse('360luyou@install')),
      (d = a.enc.Hex.parse(r).toString(a.enc.Base64)),
      (p = a.AES.decrypt(d, u, {
        iv: s,
        mode: a.mode.CBC,
        padding: a.pad.Pkcs7,
      })),
      t.abrupt('return', p.toString(a.enc.Utf8))
    )
  case 16:
  case 'end':
    return t.stop()
}虽然变量名与函数名被混淆,但可以从代码中看到采用AES加密。
首先进行加密前的处理,对rand_key进行hex,定义iv,设置模式为CBC,padding为PKCS7。
加密步骤为:
- 获取rand_key的后32位进行hex
- 对密码进行PKCS7Encode
- 最后以hex后的rand_key作为key,PKCS7Encode后的密码作为plainText,360luyou@install字符串进行hex后作为iv
- 进行AES加密
在加密结束后,将前rand_key前32位与加密字符串进行拼接作为pass传输到路由器进行登录验证。
代码实现
这里给出Python与Go的加密示例。
def gen_aes_str(rand_key: bytes):
    mode = AES.MODE_CBC
    iv = b'\x33\x36\x30\x6c\x75\x79\x6f\x75\x40\x69\x6e\x73\x74\x61\x6c\x6c'  # "360luyou@install".decode('hex')
    encryptor = AES.new(rand_key, mode, iv)
    encoder = PKCS7Encoder()
    text = "password"  # password
    pad_text = encoder.encode(text)
    cipher = encryptor.encrypt(bytes(pad_text, "utf-8"))
    return cipher// GenerateAesString 生成加密字符串
func (r *Router) GenerateAesString() (err error) {
	// 判断随机字符串是否为空
	if r.randStr == "" {
		g.Dump("randStr is empty")
		err := r.GetRandomString()
		if err != nil {
			g.Dump(err)
			return err
		}
	}
	// randKey := "fbf8a1ca3b31ace17adece7f6941a278017ff28b58200c5a153e07f5dc840b3f"
	decodeString, err := hex.DecodeString(r.randStr[32:])
	if err != nil {
		g.Dump(err)
		return
	}
	block, err := aes.NewCipher(decodeString)
	if err != nil {
		panic(err)
	}
	encryptor := cipher.NewCBCEncrypter(block, configs.DefaultAesIv)
	p7 := utils.PKCS7Encoder{BlockSize: 16}
	padded := p7.Encode([]byte(r.Password))
	cipherText := make([]byte, len(padded))
	encryptor.CryptBlocks(cipherText, padded)
	r.aesStr = hex.EncodeToString(cipherText)
	if r.aesStr == "" {
		g.Dump("aesStr is empty")
		return errors.New("aesStr is empty")
	}
	g.Dump(gtime.Now().String() + " Generate AESKey " + r.aesStr)
	return
}Go项目代码层级
完整的登陆以及获取信息由Go代码进行编写。
首先定义不同模块的接口以及结构体
type LoginMethod interface {
	Login() error
}
type AesMethod interface {
	GetRandomString() error
	GenerateAesString() error
}
type Router struct {
	Address   string
	Password  string
	state     bool
	aesIv     []byte
	inHeaders map[string]string
	randStr   string
	aesStr    string
	token     string
	cookie    string
	Headers   map[string]string
}type RouterMethod interface {
	GetRouterInfo() error
}最后以嵌套接口的形式将不同模块组合起来。
type RouterController interface {
	LoginMethod
	AesMethod
	RouterMethod
}在调用时,采用接口赋值的形式调用接口内的方法,使程序变得更加灵活且便于维护。
func main() {
	var routerMain internal.RouterController
	myRouter := internal.Router{
		Address:  configs.RouterAddress,
		Password: configs.RouterPassword,
	}
	routerMain = &myRouter
	err := routerMain.GetRandomString()
	if err != nil {
		g.Dump(err)
		return
	}
	err = routerMain.GenerateAesString()
	if err != nil {
		g.Dump(err)
		return
	}
	err = routerMain.Login()
	if err != nil {
		g.Dump(err)
		return
	}
	err = routerMain.GetRouterInfo()
	if err != nil {
		g.Dump(err)
		return
	}
}
