21

记一次用 NodeJs 实现模拟登录的思路

作者 @zwhu
原文章 @github

工欲善其事,必先利其器。

给自己定下写文章的目标后,就去找了几家博客平台来发布文章;作为一个懒人,不能所有博客文章都手动去各家平台发布,只好通过编写脚本来发布。但是除了Github提供了比较详细的Api外,其他国内的博客平台都没有提供对应的接口,但总有办法的。

下面是我对某家博客平台模拟登录流程的记录(打死我都不会说这家平台是S开头的),个人觉得挺有意思的,也能从中学到不少产品安全设计的思路。

工具

  • Babel

  • Cheerio.js

  • SuperAgent

  • Chrome 浏览器

注:工具只是实现结果的一个手段,并不一定需要掌握这些工具,只要知道它们是干嘛的就行了。

开始分析

先进入主页找到用户登录页,如下图所示:

登录

标准的登录框,在这边需要把Chrome的控制台打开,进入Network页,把 Preserve log (页面跳转也能记录日志,感谢 铁臂狗 告知)的选项勾中, 如下图所示:
Chrome 控制台

抓包分析请求,先从输入正确密码开始:

输入正确密码

正确密码的包

我把暴露隐私的两个地方打码了(这两块也是我们接下来要着重要分析的点)

可以从中看到请求头,我们先把这些请求头照抄下来

const base_headers = {
    Accept: '*/*',
    'Accept-Encoding':'gzip, deflate',
    'Accept-Language':'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,ja;q=0.2',
    'Cache-Control':'no-cache',
    Connection:'keep-alive',
    DNT:1,
    Host:'segmentfault.com',
    Origin: 'http://segmentfault.com',
    Pragma:'no-cache',
    Referer: 'http://segmentfault.com/',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest'
    }
排除法删除 Cookie

我们可以看到在请求登录的时候 Header 就已经带有 Cookie 了,这在我平常的设计中没有做过,所以我就试着把 Cookie 删后再请求,看看有什么效果。删除 Cookie 的方法如下所示:

删除Cookie的方法

利用排除法不停删除并继续试着登录,都能完成登录;直到删除 PHPSESSID 的时候发现删除之后再登陆是会报错的,所以这个 PHPSESSID 肯定是有用的(没用过PHP对这个不太了解),因此我断定这个 Cookie 是在后端作为验证登录的一个字段;因此我可以通过在登录之前先下载首页并拿到 Cookie,放到请求头上做作为模拟 Header。

获取 Cookie
import request from 'superagent'

let cookie;

req
.get(urls.mainpage)
.end((err, res) => {
   // 从上图可以看到我们需要的cookie是PHPSESSID开头的
    cookie = res.headers['set-cookie']
                .join(',').match(/(PHPSESSID=.+?);/)[1]
})
获取页面 token

本以为拿到 Cookie 之后就可以开开心心的做登录请求,然而这么简单的话这篇文章页也就没什么写的必要了。

继续分析请求 HTTP 包,可以发现在每次请求的时候,url 后面总是会带一个 queryString(图 2),我在这里耗费了不少时间,毫无头绪,只能追进源码里面摸索。

压缩后的源码

找到上图中的源码,可以看到这个源码是被压缩过的,不要着急,chrome 提供了 formatt 功能,点击最下面的{},可以对压缩的代码重排,至少是勉强可以阅读的代码了。

美化后的代码

接下来的事情就是怎么从这堆代码中抽丝剥茧找到对我们有用的信息,可是这么多的代码一步步看下来也会看到头晕脑胀,眼睛滴血。那么就试试看能不能使用查找的方式从源码中找到我需要的东西。使用快捷键 ctrl+F,键入 /login/login是作为登录的链接的,感觉上可能会有很大概率能搜到相关代码)

搜索代码

很巧的是,搜到了相关的代码。从中可以看到此网站使用了 JQuery 的 Ajax 发送相关 HTTP 请求,那么,url 便是 e.attr("action"),从下面的 DOM 结构能看到 action 是api/user/login

DOM结构

还是没有找到 queryString, 那就换个关键词试试看,这次搜索 _=(看图2,queryString 是由_=拼接起来的)

搜索代码2

从上图可以看到有7个结果,而被黄色标注出来的那行才是我们想要的。JQ 的 ajaxSend 可以在 Ajax 发送之前做一些处理。从上图可以看出,请求的时候在 url 的后面增加一个 n._ ,那就继续去找n._是什么?由于截图截少了,我就不再重新截图,从上图的第一行可以看到 _ 是window.SF.token,由此我们就摸到 token 的 G 点,整个流程明朗了许多。接下来全局搜索 window.SF.token,没找到。我知道 window 是全局变量,为什么把 token 放到 window 上?可以想多的是 token 并没有在当前的 script 标签内。接下来去 index.html 内查找:

token

找到了!可以看到 token 是被包裹在一个独立的 script 标签内,在后端生成HTML模板的时候就已经插入。

找到 token 之后就很简单了,拿到这个字符串表达式,运行,拿到token。
原理我之前写过一篇文章,移步

import cheerio from 'cheerio'
import request from 'superagent'

let cookie;


// 为什么这样做
function getToken(s) {
  let $ = cheerio.load(s)
  , text = $('body script').eq(2).text()
  , fn = new Function('window', text + ';return window.SF.token')
  , token = fn({})

  $ = null
  return token
}


req
.get(urls.mainpage)
.end((err, res) => {
   let token = getToken(res.text)
   
   // 从上图可以看到我们需要的cookie是PHPSESSID开头的
    cookie = res.headers['set-cookie']
                    .join(',')
                    .match(/(PHPSESSID=.+?);/)[1]
})
开始登录吧

拿到 token 和 Cookie ,抓包分析所需要的登录字段:

{
    mail: 'xxxxx@xx.xx', // 邮箱
    password: 'xxxxxxx', // 密码
    remember: '1'  // 是否记住登录
}

登录:

req
.get(urls.mainpage)
.end((err, res) => {
   let token = getToken(res.text)
   
   // 从上图可以看到我们需要的cookie是PHPSESSID开头的
    cookie = res.headers['set-cookie'].join(',')
                .match(/(PHPSESSID=.+?);/)[1]
    
    req
   .post(urls.login)
   .query({'_': token})
   .set(base_headers)
   .set('Cookie', cookie)
   .type('form')
   .send(conf)
   .redirects(0)
   .end((err, res) => {
        console.log(res)
    })
  })
})

总结

世上无难事只怕有心人

登录是最基础也最核心的功能,通过对登录流程的分析,基本弄清楚了此博客平台的验证机制,在分析的过程中斗智斗勇,利用自己掌握的知识一步一步破解谜题的本身就是一件很有意思的事情,以后也可以将此方法用到自己的登录流程设计中。

TODO

登录之后能施展的手段就很多了: 提问题,发表文章,创建标签等等,用到得知识都在上面说过了,按下不表。

有需要源码的同学,欢迎 Star


zwhu
1.8k 声望61 粉丝