58同城AES签名接口逆向分析

  • 2019 年 10 月 10 日
  • 筆記

背景:需要获取58同城上面发布的职位信息,其中的包括职位的招聘要求,薪资福利,公司的信息,招聘者的联系方式。(中级爬虫的难度系数)

 

  • 职位详情页分析

 某个职位详情页的链接

https://qy.m.58.com/m_detail/29379880488200/

 

 

打开以上链接并且F12进入开发者模式

 

 

 

 

我们可以看见联系方式需要登陆后才可以查看。

 

登陆后,右击鼠标查看页面的源码,发现html页面并没有电话号码,这里初步的猜测是通过ajax来加载渲染的(一般都是这种套路)

 

 

 

 

  • 全局搜索分析

由上面可见联系方式所在的div块是mobMsg和freecall,全局对这两个关键字做搜索,一步一步走下去。

<div class="msgGui">            <h3>联系方式</h3>          </div>          <dl>            <dt>联系电话:</dt>            <dd class="mobMsg">              <div id="freecall"></div>            </dd>                      <dt>电子邮箱:</dt>            <dd>[email protected]</dd>                                <dt>公司网址:</dt>            <dd class="bColr">              <a href="http://http://WWW.SHSHENXINGKEMAO.COM">http://http://WWW.SHSHENXINGKEMAO.COM</a>            </dd>                    </dl>
</div>

  

        可惜的是,这次没有找到第二个mobMsg或者freecall关键字,当然啦,不是每一次全局搜索都是奏效的。

 

       这里继续观察其他的请求,上面也是猜测是ajax请求做渲染的,故需要将注意力移到XHR模块和JS模块。

 

 

 

 

在JS模块找到如下标识的请求,可以看见请求返回的内容有一个叫virtualNum字段,顾名思义这个字段的内容可能要和我们找到电话有关系。

 

请求的链接

https://zpservice.58.com/numberProtection/biz/enterprise/mBind/?uid=29379880488200&callback=jsonp_callback2

返回的内容

{"msg":"ok","code":"0","virtualNum":"1wSca13IEbrpJNlYBR3OEQ=="}

 

这次再根据virtualNum做一次全部搜索。

 

 

 

 

这里印证了我们上面说的ajax做请求并渲染页面的做法,大多数的前端开发都是采用这种套路。

$.ajax({          type: "get",          url: "//zpservice.58.com/numberProtection/biz/enterprise/mBind/?uid=" + userId + "&callback=?",          dataType: "jsonp",          success: function(data) {              switch (data.code) {              case "0":                  insertNum(data.virtualNum);                  break;              case "4":                  $("#freecall").html("企业未公开");                  break;              case "2":                  $("#freecall").html('<a href="' + "//m.m.58.com/login/?path=" + window.location.href + '">登录后可查看</a>');                  break;              case "3":              case "6":                  insertNum(data.virtualNum);                  break;              case "5":              case "1":              default:                  console.error(data.msg);                  break              }          },          error: function(err) {              console.log(err)          }      })

 

由上面的JS我们可以知道前端页面是根据后台接口返回的结果做相应的操作,当code等于0和6的时候,就会调用insertNum()函数,那我们就继续往下看看这个insertNum函数究竟在做什么事情。

 

function insertNum(data) {          $("#freecall").html(decrypt(data))      }      function decrypt(word) {          var key = CryptoJS.enc.Utf8.parse("5749812cr3412345");          var decrypt = CryptoJS.AES.decrypt(word, key, {              mode: CryptoJS.mode.ECB,              padding: CryptoJS.pad.Pkcs7          });          return CryptoJS.enc.Utf8.stringify(decrypt).toString()      }  

 

function insertNum(data)这个函数传入刚才返回的virtualNum字段的内容,对字段的内容进行解密的操作。具体怎么解密上面的代码一目了然。

 

关于CryptoJS请大家移步至如下传送门:

https://cryptojs.gitbook.io/docs/  https://stackoverflow.com/questions/51005488/how-to-use-cryptojs-in-javascrip  https://github.com/brix/crypto-js  

  


从其官方的介绍中:

  • CryptoJS是 标准和安全密码算法的JavaScript实现

  • CryptoJS是使用最佳实践和模式在JavaScript中实现的标准安全加密算法的不断增长的集合。它们速度很快,并且具有一致且简单的界面。

  • CryptoJS说到底也就是js常用的安全密码算法的JS实现,如果对数据安全性有考虑的前端开发人员,那么这个库类都需要了解并且熟练使用。

 

综合上面的分析我们知道58同城是用CryptoJS.AES.decrypt这个方法做电话号码的加密解密。

 

  • 后端先对原来真实的号码做AES加密编码

  • 前端获取得到加密的编码根据加密的密钥(这个密钥现在是5749812cr3412345,上面截图也可以看见)在进行AES解密即可得到电话号码的原文

 

后端的java代码实现

 

 

//解密电话号码      public String decodoTel(String html,Page page){            if (html.contains("must be login")){       //如果提示登陆则返回fail              return "fail";          }          html = html.substring(html.indexOf("{"),html.lastIndexOf("}")+1);          JSONObject json = JSONObject.parseObject(html);          String virtualNum = json.getString("virtualNum");          int code = json.getInteger("code");          String telNum = StringUtils.EMPTY;            if (code==0||code ==6){              try {                  telNum = AESUtil.aesDecrypt(virtualNum, "5749812cr3412345");   //decrypKey这个密钥58现在这个阶段是这个,以后可能会变              } catch (Exception e) {                  e.printStackTrace();                  logger.error("58tongcheng decrypKey has change").tag1("58tongcheng").tag2("decrypKey change").id(page.getRequest().getTrackId()).commit();                  return  "fail";              }          }          else if (code == 2){              logger.error("58tongcheng need login").tag1("58tongcheng").tag2("need login").id(page.getRequest().getTrackId()).commit();          }            return telNum;      }

 

核心AES代码

package com.gemdata.crawler.generic.util;    import java.math.BigInteger;  import java.security.MessageDigest;  import java.security.NoSuchAlgorithmException;    import javax.crypto.Cipher;  import javax.crypto.KeyGenerator;  import javax.crypto.spec.SecretKeySpec;    import org.apache.commons.codec.binary.Base64;  import org.apache.commons.lang3.StringUtils;      /**   * AES的加密和解密*/  public class AESUtil {      //密钥 (需要前端和后端保持一致)      private static final String KEY = "5749812cr3412345";    //现在这阶段这个密钥是58同城的,以后如果遇到新的一个加密解密方法,自行修改      //算法      private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";        /**       * aes解密       * @param encrypt   内容       * @return       * @throws Exception       */      public static String aesDecrypt(String encrypt) {          try {              return aesDecrypt(encrypt, KEY);          } catch (Exception e) {              e.printStackTrace();              return "";          }      }        /**       * aes加密       * @param content       * @return       * @throws Exception       */      public static String aesEncrypt(String content) {          try {              return aesEncrypt(content, KEY);          } catch (Exception e) {              e.printStackTrace();              return "";          }      }        /**       * 将byte[]转为各种进制的字符串       * @param bytes byte[]       * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制       * @return 转换后的字符串       */      public static String binary(byte[] bytes, int radix){          return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数        }        /**       * base 64 encode       * @param bytes 待编码的byte[]       * @return 编码后的base 64 code       */      public static String base64Encode(byte[] bytes){          return Base64.encodeBase64String(bytes);      }        /**       * base 64 decode       * @param base64Code 待解码的base 64 code       * @return 解码后的byte[]       * @throws Exception       */      public static byte[] base64Decode(String base64Code) throws Exception{          //return StringUtils.isEmpty(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code);           return StringUtils.isEmpty(base64Code) ? null : new Base64().decodeBase64(base64Code);        }          /**       * AES加密       * @param content 待加密的内容       * @param encryptKey 加密密钥       * @return 加密后的byte[]       * @throws Exception       */      public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {          KeyGenerator kgen = KeyGenerator.getInstance("AES");          kgen.init(128);          Cipher cipher = Cipher.getInstance(ALGORITHMSTR);          cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));            return cipher.doFinal(content.getBytes("utf-8"));      }          /**       * AES加密为base 64 code       * @param content 待加密的内容       * @param encryptKey 加密密钥       * @return 加密后的base 64 code       * @throws Exception       */      public static String aesEncrypt(String content, String encryptKey) throws Exception {          return base64Encode(aesEncryptToBytes(content, encryptKey));      }        /**       * AES解密       * @param encryptBytes 待解密的byte[]       * @param decryptKey 解密密钥       * @return 解密后的String       * @throws Exception       */      public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {          KeyGenerator kgen = KeyGenerator.getInstance("AES");          kgen.init(128);            Cipher cipher = Cipher.getInstance(ALGORITHMSTR);          cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));          byte[] decryptBytes = cipher.doFinal(encryptBytes);          return new String(decryptBytes);      }          /**       * 将base 64 code AES解密       * @param encryptStr 待解密的base 64 code       * @param decryptKey 解密密钥       * @return 解密后的string       * @throws Exception       */      public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {          return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);      }            //MD5摘要      public static String MD5(String sourceStr)  {          String result = "";          try          {              MessageDigest md = MessageDigest.getInstance("MD5");              md.update(sourceStr.getBytes());              byte b[] = md.digest();              int i;              StringBuffer buf = new StringBuffer("");              for (int offset = 0; offset < b.length; offset++)              {                  i = b[offset];                  if (i < 0)                      i += 256;                  if (i < 16)                      buf.append("0");                  buf.append(Integer.toHexString(i));              }              result = buf.toString();          } catch (NoSuchAlgorithmException e)          {              System.out.println(e);          }          return result;      }            /**       * 测试       */      public static void main(String[] args) throws Exception {          String content = "13044154254";          System.out.println("加密前:" + content);          System.out.println("加密密钥和解密密钥:" + KEY);          String encrypt = aesEncrypt(content, KEY);          System.out.println("加密后:" + encrypt);          String decrypt = aesDecrypt(encrypt, "5749812cr3412345");          System.out.println("解密后:" + decrypt);      }  }

 

 

其中AES解码的代码段,参考了如下的链接。

 

https://www.chenwenguan.com/aes-encryption-decryption

 

最后的结果如下,直接贴上上面的代码运行即可。

  

 

 

 本文首发于本人的公众号,需要转载请把原文链接带上

https://mp.weixin.qq.com/s?__biz=MzIyNTcwMzA5NQ==&mid=2247483852&idx=1&sn=ac3903d00679779d5457c2785e0083b3&chksm=e87ae414df0d6d024107f375eb29bff075170101cdafecb8c945e5d725447f2db262d43ee3f9&token=797684618&lang=zh_CN#rd  

  

关于呼呼:会点爬虫,会点后端,会点前端,会点逆向,会点数据分析,会点算法,一个喜欢陈奕迅的?

本文的目的只有一个就是学习JS逆向分析技巧,如果有人利用本文技术进行非法操作带来的后果都是操作者自己承担,和本文以及本文作者没有任何关系。