渗透测试 | 突破前端JS加密限制

Posted by admin on 2019年12月7日 16:46

前言

现在前端开发为了提高爬虫的难度及加强安全性,都会在数据包提交前进行加密,最典型的就是传参加密,相信大家在测试的时候都遇到过,那么我们在抓取数据包并修改之后,修改之后的参数无法通过后端程序数据完整性的校验,就无法进行进一步测试。如果我们逆向解析出加密的过程,就可以模拟出相同的密文,通过后端接口的校验。

最近由于工作需要,在搜索资料的时候,学到了很多爬虫大佬们的关于JS逆向、APK逆向、代码分析等方面的经验和技巧,后续会分部分记录并总结下来。

0x01 常见加密算法

比较简单的base64、hex等这些编码就不再说了。

1.1 对称加密(加解密的密钥相同)

  • 常用算法:DES、DES3、AES
  • 根据密钥长度不同又分为:AES-128、AES-192、AES-256
  • 其中AES-192和AES-256在Java中使用需获取无政策限制权限文件
  • 加密/解密使用相同的密钥
  • 加密和解密的过程是可逆的

1.2 非对称加密(加解密密钥为公钥和私钥)

  • 常用算法:RSA
  • 使用公钥加密,使用私钥解密
  • 公钥是公开的,私钥保密
  • 加密处理安全,但是性能极差,单次加密长度有限制
  • RSA既可用于数据交换,也可用于数据校验
  • 数据校验通常结合消息摘要算法 MD5withRSA 等
两种加密算法常见结合套路:
1、随机生成密钥
2、密钥用于AES/DES/3DES加密数据
3、RSA对密钥加密
4、提交加密后的密钥和加密后的数据给服务器

1.3 信息摘要算法/签名算法

  • 常用算法:MD5、HMAC(HmacMD5、HmacSHA1、HmacSHA256)、SHA(SHA1、SHA256、SHA512)
  • 不管明文多长,散列后的密文定长
  • 明文不一样,散列后结果一定不一样
  • 散列后的密文不可逆
  • 一般用于校验数据完整性、签名 sign
  • 由于密文不可逆,所以后台无法还原,也就是说他要验证,会在后台以跟前台一样的方式去重新签名一遍。也就是说他会把源数据和签名后的值一起提交到后台。所以我们要保证在签名时候的数据和提交上去的源数据一致,这种算法特喜欢在内部加入时间戳

0x02 JS逆向流程

以登录为例的基本流程:
  1. 如果网页有跳转,必须勾选preserve log(F12-Network)防止丢包
  2. 看一下有没有框架,右键查看框架源代码(弹出式登陆界面)
  3. 登陆尽量使用错误密码,防止跳转
  4. 查看关键登陆包,分析哪些参数是加密的
  5. 使用别的浏览器分析哪些参数是固定的值
  6. 初步猜测加密的方法
  7. 搜索
    • 直接搜索参数,比如:pwd=pwd =pwd:pwd :
    • 密码框地方右键 检查 查看 id name type
  8. 找到加密的地方(重点)
  9. 进行代码调试
  10. 找出所有的加密代码
    • 从最后一步开始写起,缺啥找啥
    • 如果找的是函数的话,search 要带上 function xxx
    • 如果看到加密的地方有个类,并且之后是用 prototype 把方法加在原生对象上的话,要把 所有加在原生对象上的方法都找出来
    • 函数找多了没关系,只要不报错不会影响结果,但是不能找少了

0x03 实例操作

打开网站,抓包

image.png

修改个数据,比如修改num为1000

image.png

有些杠精该说了:我渗透测试改这有毛用。

是没用,只是以此为例。。。

可以看到,修改过参数数据后,后端数据校验之后不合法,所以没有返回数据。

打开测试网站 -> F12控制台 -> 切换至 XHR

这里我们看下需要做的有什么?

  1. 请求中有token(t明显是时间戳),token和数据不匹配后端不返回数据
  2. 返回的数据是加密的,需要解密

接下来我们就定位具体的加密函数和解密函数。

按照上面提到的流程步骤

打开控制台 -> source ->搜索

搜索加密参数名 token

根据搜索结果的文件名判断,基本上就是第二个文件,点击打开

token的生成代码

var token = md5(String(page) + String(num) + String(timestamp));

设置断点,刷新

成功进入断点,没毛病了

根据加密函数,编写脚本

可以看到,生成的token和URL中的一致,至此,加密部分完成。

解密部分同样的道理,搜索返回包中的参数,直接搜索list发现有点多,不太好观察,还有一种方法

可以看到数据部分html的id为ip-list,再次搜索

成功找到解密数据包的代码

设置断点,进一步确认

没毛病,可以看到decode_str后就开始出现我们需要的明文数据了,所以这里的 decode_str 就是我们要的解密方法。剩下就是分析代码,编写解密脚本了。

function decode_str(scHZjLUh1) {
    scHZjLUh1 = Base64["\x64\x65\x63\x6f\x64\x65"](scHZjLUh1);
    key = '\x6e\x79\x6c\x6f\x6e\x65\x72';
    len = key["\x6c\x65\x6e\x67\x74\x68"];
    code = '';
    for (i = 0; i < scHZjLUh1["\x6c\x65\x6e\x67\x74\x68"]; i++) {
        var coeFYlqUm2 = i % len;
        code += window["\x53\x74\x72\x69\x6e\x67"]["\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65"](scHZjLUh1["\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74"](i) ^ key["\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74"](coeFYlqUm2))
    }
    return Base64["\x64\x65\x63\x6f\x64\x65"](code)
}

先运行下看看

报错,提示Base64未定义,设置断点,找到Base64的具体代码

复制粘贴进代码,再次运行

艹,提示Windows未定义,根据流程,缺啥补啥,debug,找对应的值

可以看到分别对应的是StringfromCharCode

那就是调用了String.fromCharCode方法了,替换掉,再次运行

bingo~

成功解密获取到明文数据。

0x04 Python实现加密方法合集

关于上述第一部分的常见加密算法,GitHub有对应的仓库,直接可以用的

GitHub:https://github.com/dhfjcuff/R-A-M-D-D3-S-M-H/

# -*- coding:utf-8 -*-
import base64
import rsa
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from pyDes import des, CBC, PAD_PKCS5
from Crypto.Cipher import DES3
import hashlib
import hmac


class USE_AES:
    """
    AES
    除了MODE_SIV模式key长度为:32, 48, or 64,
    其余key长度为16, 24 or 32
    详细见AES内部文档
    CBC模式传入iv参数
    本例使用常用的ECB模式
    """

    def __init__(self, key):
        if len(key) > 32:
            key = key[:32]
        self.key = self.to_16(key)

    def to_16(self, key):
        """
        转为16倍数的bytes数据
        :param key:
        :return:
        """
        key = bytes(key, encoding="utf8")
        while len(key) % 16 != 0:
            key += b'\0'
        return key  # 返回bytes

    def aes(self):
        return AES.new(self.key, AES.MODE_ECB) # 初始化加密器

    def encrypt(self, text):
        aes = self.aes()
        return str(base64.encodebytes(aes.encrypt(self.to_16(text))),
                   encoding='utf8').replace('\n', '')  # 加密

    def decodebytes(self, text):
        aes = self.aes()
        return str(aes.decrypt(base64.decodebytes(bytes(
            text, encoding='utf8'))).rstrip(b'\0').decode("utf8"))  # 解密


class USE_RSA:
    """
    生成密钥可保存.pem格式文件
    1024位的证书,加密时最大支持117个字节,解密时为128;
    2048位的证书,加密时最大支持245个字节,解密时为256。
    加密大文件时需要先用AES或者DES加密,再用RSA加密密钥,详细见文档
    文档:https://stuvel.eu/files/python-rsa-doc/usage.html#generating-keys
    """
    def __init__(self, number=1024):
        """
        :param number: 公钥、私钥
        """
        self.pubkey, self.privkey = rsa.newkeys(number)

    def rsaEncrypt(self, text):
        """
        :param test: str
        :return: bytes
        """
        content = text.encode('utf-8')
        crypto = rsa.encrypt(content, self.pubkey)
        return crypto

    def rsaDecrypt(self, text):
        """
        :param text:bytes 
        :return: str
        """
        content = rsa.decrypt(text, self.privkey)
        con = content.decode('utf-8')
        return con

    def savePem(self, path_name, text):
        """
        :param path_name: 保存路径
        :param text: str
        :return:bytes
        """
        if "PEM" in path_name.upper():
            path_name = path_name[:-4]
        with open('{}.pem'.format(path_name), 'bw') as f:
            f.write(text.save_pkcs1())

    def readPem(self, path_name, key_type):
        """
        :param path_name: 密钥文件
        :param key_type:类型 
        :return: 
        """
        if 'pubkey' in key_type:
            self.pubkey = rsa.PublicKey.load_pkcs1(path_name)
        else:
            self.privkey = rsa.PublicKey.load_pkcs1(path_name)
        return True

    def sign(self, message, priv_key=None, hash_method='SHA-1'):
        """
        生成明文的哈希签名以便还原后对照
        :param message: str
        :param priv_key:
        :param hash_method: 哈希的模式
        :return:
        """
        if None == priv_key:
            priv_key = self.privkey
        return rsa.sign(message.encode(), priv_key, hash_method)

    def checkSign(self, mess, result, pubkey=None):
        """
        验证签名:传入解密后明文、签名、公钥,验证成功返回哈希方法,失败则报错
        :param mess: str
        :param result: bytes
        :param pubkey: 
        :return: str
        """
        if None == pubkey:
            pubkey = self.privkey
        try:
            result = rsa.verify(mess, result, pubkey)
            return result
        except:
            return False


class USE_DES:
    """
    des(key,[mode], [IV], [pad], [pad mode])
    key:必须正好8字节
    mode(模式):ECB、CBC
    iv:CBC模式中必须提供长8字节
    pad:填充字符
    padmode:加密填充模式PAD_NORMAL or PAD_PKCS5
    """
    def __init__(self, key, iv):
        if not isinstance(key, bytes):
            key = bytes(key, encoding="utf8")
        if not isinstance(iv, bytes):
            iv = bytes(iv, encoding="utf8")
        self.key = key
        self.iv = iv

    def encrypt(self, text):
        """
        DES 加密
        :param text: 原始字符串
        :return: 加密后字符串,bytes
        """
        if not isinstance(text, bytes):
            text = bytes(text, "utf-8")
        secret_key = self.key
        iv = self.iv
        k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
        en = k.encrypt(text, padmode=PAD_PKCS5)
        return en

    def descrypt(self, text):
        """
        DES 解密
        :param text: 加密后的字符串,bytes
        :return:  解密后的字符串
        """
        secret_key = self.key
        iv = self.iv
        k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
        de = k.decrypt(text, padmode=PAD_PKCS5)
        return de.decode()


class USE_DES3:
    """
    new(key, mode, *args, **kwargs)
    key:必须8bytes倍数介于16-24
    mode:
    iv:初始化向量适用于MODE_CBC、MODE_CFB、MODE_OFB、MODE_OPENPGP,4种模式
        ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB``长度为8bytes
        ```MODE_OPENPGP```加密时8bytes解密时10bytes
        未提供默认随机生成
    nonce:仅在 ``MODE_EAX`` and ``MODE_CTR``模式中使用
            ``MODE_EAX``建议16bytes
            ``MODE_CTR``建议[0, 7]长度
            未提供则随机生成
    segment_size:分段大小,仅在 ``MODE_CFB``模式中使用,长度为8倍数,未指定则默认为8
    mac_len: 适用``MODE_EAX``模式,身份验证标记的长度(字节),它不能超过8(默认值)
    initial_value:适用```MODE_CTR```,计数器的初始值计数器块。默认为**0**。
    """
    def __init__(self, key):
        self.key = key
        self.mode = DES3.MODE_ECB

    def encrypt(self, text):
        """
        传入明文
        :param text:bytes类型,长度是KEY的倍数
        :return:
        """
        if not isinstance(text, bytes):
            text = bytes(text, 'utf-8')
        x = len(text) % 8
        text = text+b'\0'*x
        cryptor = DES3.new(self.key, self.mode)
        ciphertext = cryptor.encrypt(text)
        return ciphertext

    def decrypt(self, text):
        cryptor = DES3.new(self.key, self.mode)
        plain_text = cryptor.decrypt(text)
        st = str(plain_text.decode("utf-8")).rstrip('\0')
        return st


def USE_MD5(test):
    if not isinstance(test, bytes):
        test = bytes(test, 'utf-8')
    m = hashlib.md5()
    m.update(test)
    return m.hexdigest()


def USE_HMAC(key, text):
    if not isinstance(key, bytes):
        key = bytes(key, 'utf-8')
    if not isinstance(text, bytes):
        text = bytes(text, 'utf-8')
    h = hmac.new(key, text, digestmod='MD5')
    return h.hexdigest()


def USE_SHA(text):
    if not isinstance(text, bytes):
        text = bytes(text, 'utf-8')
    sha = hashlib.sha1(text)
    encrypts = sha.hexdigest()
    return encrypts


if __name__ == '__main__':
    aes_test = USE_AES("assssssssdfasasasasa")
    a = aes_test.encrypt("测试")
    b = aes_test.decodebytes(a)
    rsa_test = USE_RSA()
    a = rsa_test.rsaEncrypt("测试加密")
    b = rsa_test.rsaDecrypt(a)
    des_test = USE_DES(b"12345678", b"12345678")
    a = des_test.encrypt("测试加密")
    b = des_test.descrypt(a)
    des3_test = USE_DES3(b"123456789qazxswe")
    a = des3_test.encrypt("测试加密")
    b = des3_test.decrypt(a)
    md5_test = USE_MD5("测试签名")
    hmac_test = USE_HMAC("123456", "测试")
    sha_test = USE_SHA("测试加密")

参考资料

公众号:咸鱼学python

公众号:小周码字