密码学基础算法及WEB前端加密

其实对于后端来说,其实无论前端是直接传输明文,还是通过一定的加密手段来传输明文,从数据层面来讲,都是明文,并且前端代码是运行在用户本地,代码都能扒下来(一般都会出现在前端的js代码中,需要我们对js代码进行一个较为细致的分析),什么加密算法都是用户可见的(混淆,散列,加密),但是作为用户隐私数据的保护,和增加攻击难度层面来讲,仍有它存在的必要。

所以前端加密的局限性:即使数据在客户端加密,攻击者仍然可以查看加密逻辑和密钥,如果实现不当反而带来更多风险。所以要建议结合后端加密,使用HTTPS,并正确管理密钥,不能在前端直接使用硬编码密钥,可以动态从后端获取临时密钥,可通过 HTTPS 接口按需获取,或使用环境变量。另外,关于TLS的重要性,前端加密不能替代传输层的安全,必须确保整个通信过程中使用HTTPS。

Base64

就是一种用64个Ascii字符来表示任意二进制数据的方法。主要用于将不可打印的字符转换成可打印字符,或者简单的说将二进制数据编码成Ascii字符。Base64是网络上最常用的传输8bit字节数据的编码方式之一。其最大特征就是加密串末尾会出现等号。

使用场景往往是:在http环境下,将一些较长的标识信息或二进制数据转化为适合传输的字符串编码

或者对一些较小的图标图片使用base64格式,以减少网络请求次数,降低服务器压力

加密原理

首先,需要准备一个包含64个字符的表格(如下表),0~63分别对应了唯一一个字符,如18对应的是S。

索引值对应字符索引值对应字符索引值对应字符索引值对应字符
0A16Q32g48w
1B17R33h49x
2C18S34i50y
3D19T35j51z
4E20U36k520
5F21V37l531
6G22W38m542
7H23X39n553
8I24Y40o564
9J25Z41p575
10K26a42q586
11L27b43r597
12M28c44s608
13N29d45t619
14O30e46u62+
15P31f47v63/

然后加密的原理就是将原字符串进行二进制转换,每个字符对应8位二进制位,那么然后将转换得到的二进制字符串每6位取为一组,转换为10进制数索引,然后根据表格中索引的对照位置进行替换,从而形成Base64编码,由于是由8位一组变成6位一组,因此,Base64编码之后的文本,要比原文长大约三分之一。也就是说,每原长三个字节的数会变成四个字节的数。(3×8=4×6)

那么对于位数不足三个字节的情况:如原长为1个字节-8bit位,若转换成base64编码,每6bit一组,则第二组缺少4位,用0补齐,剩下两组则取等号。如原长为2个字节-16bit,若转换为base64编码,每6bit一组,则第三组会缺少2位,也用0补齐,最后一组则取等号。

示例:

第一步:以上述图片为例:“M”、“a”、”n”对应的ASCII码值分别为77,97,110,对应的二进制值分别是01001101、01100001、01101110。由此串联组成一个24位的二进制字符串。

第二步:如图红色框,将24位每6位二进制位一组分成四组。

第三步:在上面每一组前面补两个0,扩展成32个二进制位,此时变为四个字节:00010011、00010110、00000101、00101110。分别对应的值(Base64编码索引)为:19、22、5、46。

第四步:用上面的值在Base64编码表中进行查找,分别对应:T、W、F、u。因此“Man”Base64编码之后就变为:TWFu。

对于位数不足的情况:

一个字节时:一个字节共8个二进制位,依旧按照规则进行分组。此时共8个二进制位,每6个一组,则第二组缺少4位,用0补齐,得到两个Base64编码,而后面两组没有对应数据,都用“=”补上。因此,上图中“A”转换之后为“QQ==”;

两个字节时:两个字节共16个二进制位,依旧按照规则进行分组。此时总共16个二进制位,每6个一组,则第三组缺少2位,用0补齐,得到三个Base64编码,第四组完全没有数据则用“=”补上。因此,上图中“BC”转换之后为“QKM=”;

解密

Base64是一个存在解密的可逆加密算法,解密原理就是与加密原理相反而驰,先将编码后的base64转换成二进制字符串,在转换成8位2进制字符串时,将字符串最前面两位0都去掉,然后拼接形成一个全新的二进制字符串,然后取每8位二进制一组,等号则没有对应的字符串取空值,然后将每8位二进制字符转为10进制后去ASCII表中取出相应的原文。

但是在一些CTF比赛中,编码往往会忽视一个重要的问题,编码表,编码表一般是采用默认方式,也有人会进行自定义,那么这时候就要跟据自定义的编码表来进行还原解密了。

工具推荐:

Base64 解码和编码 – 在线

自定义base64编解码、二进制转可打印字符、在线base2、base4、base8、base16、base32、base64

前端加密示例

<html>
    <head>
        <title>前端的base64使用方法</title>
    </head>
    <body>
    </body>
<script>
var str = "hello";
var str64 = window.btoa("hello");
console.log("字符串是:"+str);
console.log("经base64编码后:"+str64);
console.log("base64解码后:"+window.atob(str64));
</script>
</html>

MD5加密

一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(又称hash值),用于确保信息传输完整一致。特点是加密后的结果长度一致且不可逆。常常配合盐值、双重加密等方式进行,盐值一般不会存储在前端,往往会放在数据库之中进行存储。

应用环境

  • 密码存储,将用户明文密码用md5加密后再存储于数据库中,防止泄露后被人用作密码撞库。
  • 签名校验,将数据经过本地md5加密生成签名,与服务器端md5加密后的签名进行比对,来判断数据是否合法安全。

示例

hello ==> 5d41402abc4b2a76b9719d911017c592
helloo ==> b373870b9139bbade83396a49b1afc9a

解密

由于同一字符串在md5加密后的结果是固定的,比如123456在md5加密后始终是e10adc3949ba59abbe56e057f20f883e,所以通过比对加密后的结果就能反向判断密码,理论上是可以通过遍历来暴力破解的。也就是我们所谓的撞库攻击。目前还有一种叫彩虹表攻击跟撞库类似,但是效果显著。

彩虹表往往用于破解hash,效果非常显著。可以参考以下文章:

利用彩虹表破解Hash – 狂客 – 博客园

网络攻防实验:离线攻击工具——彩虹表破解_kali彩虹表-CSDN博客

密码破解之王:Ophcrack彩虹表(Rainbow Tables)原理详解(附:120G彩虹表下载)

也可以直接用一些在线工具进行破解:

md5在线解密破解,md5解密加密

MD5免费在线解密破解_MD5在线加密-SOMD5

前端加密示例

MD5.js是通过前台js加密的方式对用户信息,密码等私密信息进行加密处理的工具,也可称为插件。

MD5共有6种加密方法:
1, hex_md5(value)
2, b64_md5(value)
3, str_md5(value)
4, hex_hmac_md5(key, data)
5, b64_hmac_md5(key, data)
6, str_hmac_md5(key, data)

下载地址:https://links.jianshu.com/go?to=http%3A%2F%2Ffiles.cnblogs.com%2Ftuyile006%2Fmd5.rar

下载好了用script 标签引入使用
<script src="md5/md5.js"></script>"></script>
    <script>
        var code = "123456";
        var username = "123456";
        var password = "123456";
        var str1 = hex_md5("123456");
        var str2 = b64_md5("123456");
        var str3 = str_md5("123456");
        var str4 = hex_hmac_md5(code,code);
        var str5 = b64_hmac_md5(username,username);
        var str6 = str_hmac_md5(password,password);
        console.log(str1);            // e10adc3949ba59abbe56e057f20f883e
        console.log(str2);            // 4QrcOUm6Wau+VuBX8g+IPg
        console.log(str3);            // áÜ9IºY«¾VàWò��>
        console.log(str4);            // 30ce71a73bdd908c3955a90e8f7429ef
        console.log(str5);            // MM5xpzvdkIw5VakOj3Qp7w
        console.log(str6);            // 0Îq§;Ý��9U©��t)ï
</script>

SHA-1与SHA-256

这两者都与MD5类似,属于hash算法。

SHA-1是一种安全哈希算法,产生160位(20字节)的哈希值。相比于MD5,安全性更高,但速度稍慢。

使用场景:数字签名、文件校验等。(SHA-1已被认为不安全,建议使用SHA-256或更高版本的SHA算法)

SHA-256是SHA-2算法族中的一员,产生256位(32字节)的哈希值。比SHA-1更安全,速度也相当快。

使用场景:广泛用于密码存储、文件校验、数字签名等。

示例代码(使用Node.js的crypto模块):
const hash = crypto.createHash('sha256').update('Hello, World!').digest('hex');
console.log('SHA-256 Hash:', hash); // 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3

AES对称加密算法

常见的对称加密算法包括很多类型如DES、3DES,但使用更广泛的当属AES。

对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。它要求发送方和接收方在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都可以对他们发送或接收的消息解密,所以密钥的保密性对通信的安全性至关重要。所以密钥是不允许出现在前端代码中的。且AES支持多种密钥长度(如128位、192位、256位),这里的位指的是bit位,也是说128位对应16长度的密钥

使用场景:包括本地数据加密、HTTPS通信、网络传输、区块链钱包加密等。

加密逻辑

即简单来说,就是双方协商一个相同长度的密钥,然后发送方加密明文,传输后由接收方解密密文。

AES 采用分组加密方式,即它一次处理固定大小的数据块(128 比特)。AES 主要包含以下几个核心操作:

🔹 1. 轮密钥加(AddRoundKey)
每一轮加密都要将数据与子密钥进行异或(XOR)运算,确保数据的混淆性。

🔹 2. 字节代换(SubBytes)
使用 S 盒(Substitution Box)进行字节级别的替换,增强密码的非线性性。

🔹 3. 行移位(ShiftRows)
数据的行进行循环移位,使其扩散,增加加密的复杂度。

🔹 4. 列混淆(MixColumns)(仅在前 N-1 轮执行)
对数据列进行数学变换,进一步增强数据的混淆性。

🔹 5. 轮密钥扩展(Key Expansion)
通过特定的密钥调度算法,从初始密钥生成多个轮密钥,确保不同轮次的加密数据不同。

AES-128 需要执行 10 轮(AES-192 执行 12 轮,AES-256 执行 14 轮),最终得到密文。解密过程是加密的逆过程。

填充方式

AES加密解密都采用相同的填充方式,要想了解填充的概念,我们先要了解AES的分组加密特性。

如下图所示:

AES算法在对明文加密的时候,并不是把整个明文一股脑加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit。

这些明文块经过AES加密器的复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密结果。

但是这里涉及到一个问题:假如一段明文长度是192bit,如果按每128bit一个明文块来拆分的话,第二个明文块只有64bit,不足128bit。这时候怎么办呢?就需要对明文块进行填充(Padding)。因此AES提供了各种填充方式:

NoPadding:不做任何填充,但是要求明文必须是16字节的整数倍。

PKCS5Padding(默认):如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则补全为{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}

ISO10126Padding:如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为{1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}

ZerosPadding:全部填充0x00,无论缺多少全部填充0x00,已经是128bits倍数仍要填充

加密模式

ECB(Electronic Codebook)模式:将每个分组独立加密,适用于对称性较弱的数据。ECB模式有一个显著的安全问题:如果使用相同的密钥,那么相同的明文块就会生成相同的密文块,不能很好的隐藏数据模式。这听起来没什么大事,但事实上这对数据安全是一个很大的威胁,下面这张图很明显的体现出了这个问题:

CBC(Cipher Block Chaining)模式:在CBC中,每个明文块要先与前一个密文块进行异或后再加密,每个密文块都依赖于前面的所有明文块。对于第一个密码块,则用初始向量IV与之异或得到密文块。这个方法看起来很不错,但有一个缺点:加密过程是串行的,不能并行化,速度比较慢,但是解密可以并行。另外,如果密文的某一位被修改了,只会使这个密文块所对应的明文块完全改变并且改变下一个明文块的对应位,安全性仍然有一定的欠缺。

CFB(密码反馈)模式:前一个密文分组会被送入密码算法的输入端,再将输出的结果与明文做异或。与ECB和CBC模式只能够加密块数据不同,CFB能够将块密文(Block Cipher)转换为流密文。

CTR(Counter)模式:通过使用计数器生成密钥流,将密钥流与明文进行异或操作得到密文。

COUNTER是整个CTR模式的核心所在。它是由IV经过一定的规则之后生成的一段数据,长度与数据块的长度相等。接着我们要选定一个数m,这个m是用于确定计数器中累加部分的大小的,通常取块大小的一半,块大小是奇数就四舍五入(当然对于AES并没有这个问题)。初始的计数器COUNTER1长度固定的任意一个随机字节序列,而不是像想象中那样一段随机数后面跟着一段0。现在我们假设块大小b=8bits,m=5bits (这里只是为了便于举例才取8bits和5bits,在AES-CTR中通常是取16bytes和8bytes),我们用*表示随机值部分,初始计数器为***11110,那么最终计数器就是这样的:

***11110
***11111
***00000
***00001
***00002
......
也就是说,随机部分内容不变,其他部分每次+1,如果超出了范围就从0开始重新来。

解密

解密过程注意保证填充方式、IV向量、加密模式都要与加密过程保持一致。工具:

AES 加密/解密 – 锤子在线工具

AES加密解密

在线AES加密-AES解密

示例代码

下载crypto-js.js 引入使用,连接如下:
https://links.jianshu.com/go?to=https%3A%2F%2Fcdnjs.com%2Flibraries%2Fcrypto-js

var aseKey = "12345678"     //秘钥必须为:8/16/32位
var message = "80018000142";
//加密
var encrypt = CryptoJS.AES.encrypt(message, CryptoJS.enc.Utf8.parse(aseKey), {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.Pkcs7
}).toString();
console.log(encrypt);    //VKrZlqykem73x8/T2oCfCQ==

//解密
var decrypt = CryptoJS.AES.decrypt(encrypt, CryptoJS.enc.Utf8.parse(aseKey), {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
console.log(decrypt);    //80018000142

RSA非对称加密算法

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。这里还有一种ECC加密一般是移动设备使用较多,这里留个伏笔,待之后完善。

特点:

  • 优点:非对称加密与对称加密相比其安全性更好
  • 缺点:加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
  • 使用场景:https 会话前期、CA 数字证书、信息加密、登录认证等特别需要公钥公开的地方

加密原理:

RSA 加密算法是非对称加密算法最常见的一种。

// 使用公钥加密
var publicKey = 'public_key_123';
var encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
var encrypted = encrypt.encrypt('Hello World');

// 使用私钥解密
var privateKey = 'private_key_123';
var decrypt = new JSEncrypt();
decrypt.setPrivateKey(privateKey);
var uncrypted = decrypt.decrypt(encrypted);

解密

工具:RSA在线测试工具 – Toolzl工具

示例代码

RSA加密:
  var encryptor = new JSEncrypt()  // 创建加密对象实例
  //之前生成的公钥,复制的时候要小心不要有空格(此处把密钥省略了,自己写的时候可把自己生成的公钥粘到对应位置)
  var pubKey = '-----BEGIN PUBLIC KEY-----MIGfMA0......AQAB-----END PUBLIC KEY-----'
  encryptor.setPublicKey(pubKey)//设置公钥
  var rsaPassWord = encryptor.encrypt('要加密的内容')  // 对内容进行加密

RSA解密:
  var decrypt = new JSEncrypt()//创建解密对象实例
  //之前生成的秘钥(把自己生成的密钥钥粘到对应位置)
  var priKey  = '-----BEGIN RSA PRIVATE KEY-----MIICXAIBA......AKBgQC1QQWRk=-----END RSA PRIVATE KEY----'
  decrypt.setPrivateKey(priKey)//设置秘钥
  var uncrypted = decrypt.decrypt(encrypted)//解密之前拿公钥加密的内容

实战技巧

渗透测试高级技巧(一) :分析验签与前端加密 | Yak Program Language

渗透测试高级技巧(二):对抗前端动态密钥与非对称加密防护 | Yak Program Language

渗透测试高级技巧(三):被前端加密后的漏洞测试 | Yak Program Language

这三篇文章目前来说有点啃不动,待之后来慢慢细品

工具推荐:

f0ng/autoDecoder: Burp插件

Release jsEncrypter v0.3.2 · c0ny1/jsEncrypter

Burp插件 – BurpCrypto

浏览器算法加解密插件 Ctool

https://github.com/cilame/v_jstools

使用案例:

burp插件–爆破前端加密(jsEncrypter、BurpCrypto)-CSDN博客

示例

以下面在网上找到的带加解密网关的一个比较基础的图书管理系统,项目地址为:

http://39.98.108.20:8085/ —演示DEMO

0ctDay/encrypt-decrypt-vuls: 加解密逻辑漏洞靶场,可根据需求自行修改

先了解一下加解密流程

用户输入明文消息在被JS提取后, 通过JS中定义的加密方法进行加密并制作请求包进行请求。 发送到后端后,一般来说由加解密网关进行解密后, 发送到真正提供服务的服务器上。

​ 加解密网关本质其实就是一种反向代理,除了本身转发数据包的功能以外,还对数据包进行解密。

按照这种部署模式,有两个优势:

  1. 如果新增分布式系统, 无需重复实现加解密功能, 直接使用作为该网关的下游服务即可。
  2. 在springcloud gateway和 分布式系统之间部署安全设备, 可以检测明文流量识别攻击行为。
  3. 这样部署也避免了后端需要对密文进行处理的繁杂性,做到后端直接处理明文业务逻辑。
  4. 同时保证最小权限网关与后端服务通过内网通信,禁止外网直接访问后端,保证了明文数据不经过公网

改包的防范

先汇总一下目前流行的防止改包方式

1. 请求参数和路径的加密(靶场未实现)

如果原始请求是GET请求,或防止访问者获取请求路径,通常会将用户实际的请求路径和GET请求参数封装都封装为POST请求的请求体,通过加解密网关再还原为原始GET请求传入后端分布式服务上。 在APP中比较常见。表现的形式通常为: 抓包后发现访问任何功能都是同一路径,并且请求全为密文

2. 请求体的加密

这类在纯web中最常见, 通常仅仅加密接口请求的请求体内容,但有以下几类加密问题。

使用固定密钥 — 顾名思义, 这种情况一般JS中会存储密钥, 属于最简单的一种
使用动态密钥 — JS中不存储,一般用户第一次请求后将密钥加密写入COOKIE或本地存储中, 这类加密追踪难度较大。
对称加密 — 加解密数据包内容同一套密钥
非对称加密 — 加密一套解密一套
算法 — 算法就不是特别固定了, 常见的诸如AES RSA等, 也遇到过使用国密算法或一些冷门算法。

3. 签名

签名的应用也十分广泛,app,小程序和现在许多web中均存在,签名的构成主要是以下几点

RequestId — 为了防止重放攻击, 客户端生成随机RequestId 服务端接收后保存至Redis中, 如果再次接收到此RequestID, 则视为非法请求
时间戳 — 添加时间戳的超时时间, 一旦超时, 原始数据包失效
签名本身 — 通过 requestId + 原始请求体或请求参数 + 时间戳 + 盐值合并生成哈希值 从而保证以上参数的有效性和唯一性

JS逆向

 JS逆向老生常谈的问题了, 需要懂得基础的JS知识,和调试方法, 这里主要就不讲解如何详细的逆向了,我们直接切入正题: 如何解决数据包的修改问题。具体文章可以参考我的另一篇文章。

关于如何找到JS中的加解密的方法,很多新手会认为一定得找到加解密函数。其实不然, 老手都会告诉你无论是APP还是JS中可以先找到明文点

所谓明文点,就是在前端进行复杂的请求操作时,肯定会经过一系列从A函数–>B函数–>C函数–>D函数–>E函数之类的流程, 那么在这个流程中,假设D函数是加密函数,那么ABC函数中原始请求参数均是明文的,这就是明文点,找到明文点后再一步步调试, 其实就能顺腾摸瓜找到加密函数了。

推荐工具:https://github.com/cilame/v_jstools

使用教程:网页 js 逆向分析 ( v_jstools )、jshook ( 安卓用js实现Hook )、神之手、算法助手、hookui

能够一键监测JS中指定函数的调用

演示

1. 首先安装好v_jstools

进行配置, 选择需要挂钩的函数即可,然后点击挂钩总开关。
具体挂钩什么函数,需要根据具体需求, 千万不要全部都勾上,那样信息流就太多了不利于我们的筛选

2. 打开挂钩功能, 打开浏览器控制台, 刷新页面

3. 发送数据包,查看提示

这里就可以看到, 针对请求包, 我们找到了请求的明文点

4. 跟进对应的JS文件

在该代码处打上断点, 跟踪后续的处理

 重新发送数据包, 并点击步过, 我们就可以看到 名字为 n 的变量就是我们的提交内容

此时n还是明文, 那么我们继续步过, 经过 t.data = l(n)后,data内容为密文,
经过实际请求包的比较, 发现t.data 即为 加密后的内容, 那么l() 函数即为加密函数

所以我们跟进l() 函数中看看,发现l函数即为加密函数,也顺便发现了解密函数就在下面,其中t参数为原始的内容,f参数为密钥,h为密码。

​ 再回到刚刚的起始点, 也可以发现requestID算法,时间戳算法 和 签名算法

当然这算是个比较简单的例子,算法和对象的跟踪的都比较直接

tips:

  • 搜索:如果能搜索到当然好,比如这个环境可以直接通过搜索AES的方式找到上下文。但现在大部分webpack项目都自带混淆,在生产环境中许多变量和字符无法直接搜索,即使搜索到了阅读上下文也看不懂代码到底是个什么意思。
  • 明文点:在这个过程中我们跟踪的两个函数都是明文点,一个是v_jstools 提示的函数位置 u.interceptors.request.use((function(t){} ,还有一个是l()函数。生产情况下,我们可能无法直接分析出该函数的作用,即便看不懂,也是大有用处,这点我们后面娓娓道来。

修改数据包

找到明文点后,我们开始着手数据包的修改了,那么我们来了解一下主流的修改方式。

  • 修改当前的数据包:浏览器发包后,代理到burp上或通过其他的形式修改这个数据包的内容,主要针对当前数据包的修改(场景: 分析请求参数、添加额外参数、绕过前端校验等等)
  • 主动发包的加密与解密:脱离浏览器, 主动发包并加密, 对响应的数据包解密 (场景: 自动化工具插入漏洞payload、暴力破解、重放测试等)

1、修改当前数据包

直接在明文点处修改

这里有个账号: test 密码:123 首先我们在表单处输入 test 密码 1234,很明显我们是无法登录的。这时我们进入到调试中,走到加密前的一步,直接在作用域中修改。这个方式需要在加密方式和一些签名的步骤之前,否则可能修改无效

然后继续运行,即可登录成功了

JS-forward修改

个人认为仅仅改包的话,是一个比较万金油的办法。首先我们了解一下JS-forward运行原理,简单来说就是在明文点处插入一段JS代码,这段代码先通过AJAX请求将现有的请求内容发送给burpsuite,burpsuite拦截并修改内容后,返回到原始变量中,优点是操作比较统一,如果明文点正确,后续所有的改包操作都可以在burpsuite中进行,大概就是这样一个流程

所以我们来实现, 步骤有一丢丢麻烦,请耐心看完

找到明文点:确认明文变量名,然后启动JS-forward,这里我们需要将变量名向前看一点,确认明文变量就是t.data

启动JS-forward;输入变量名: t.data;输入数据类型: json (JS原始对象也可设置为JSON);输入请求标识: REQUEST(这里的请求标识仅作为标识使用, 无任何意义, 主要是为了在bp中区分请求包和响应包)

输入$end 结束后, 会监听2个端口 分别是38080, 28080, 还会生成一段JS代码我们留作后续使用。

插入JS代码:JS-forward 的使用尽量在明文点的函数第一行插入相关代码,因为不知道后续代码会做什么样的操作。

具体的插入方式:

1:找到F12–源代码–替换(覆盖)–点击选择文件夹–选择我们硬盘中一个空文件夹

如果浏览器有提示点击允许

2:在 网页–明文点JS文件处–右键–替换内容;因为只有这样才能修改JS中的代码

3:将JS-forward中生成的代码,复制到函数第一行,Ctrl+S保存

打开burpsuite:关闭调试功能或关闭F12,刷新页面,再次发包时即可接收到明文信息

(注意:此功能与浏览器–burpsuite代理无关,浏览器的代理可不设置为burpsuite、另外在实际测试过程中谷歌浏览器会报CORS错误,edge正常,具体原因不明,以后有机会再分析)

2、主动发包的加密与解密

​ 以上方法只适合修改浏览器的提交操作后的数据包修改:

  • 优点:是简单易上手,就算是复杂的加密环境,只要找到明文点,后续工作不太复杂。
  • 缺点:是无法应对主动发包的情况,比如要使用被动扫描工具,暴力破解,重放测试等需求的时候,无法自动化完成。

所以我们介绍第二类的解决方案,主动发包的加密和解密更加复杂,需要读懂目标JS代码环境中防范改包的一些业务逻辑,如果目标的JS代码混淆和加密并不是特别厉害,还是可以一试的。

工具推荐:JS-RPC:https://github.com/jxhczhl/JsRpc

所谓RPC,翻译过来是远程调用的意思,简单来说就是搭建一个桥梁让两个不同的应用系统之间一方能主动调用另外一方的api或函数。

​ 我们知道浏览器中的加解密都是通过JS实现的,但如果想脱离浏览器在本地运行JS代码最大一个问题就是如何调用浏览器的api。举个例子: 比如我们想在python中执行JS中的解密函数,我们通常是两个方法:

  1. 读懂JS加密函数的内容,在python中通过python代码使用同样的逻辑来实现。
  2. 通过execJs,selenium等框架执行指定的JS代码,理想状态是好的。但是,如果目标环境的加密很复杂,又伴随着一些复杂的对象操作,需要解析各种变量以及补环境来满足函数调用,意味着可能我们还没开始渗透就已经脱了几层皮了。

​ 所以我们可以使用这款工具提高渗透测试前期的效率。

JS-RPC这款工具的工作原理就是在控制台中执行一段代码,通过websocket与本地的python服务端相连。这样一来如果python中想要执行代码,只需要通过RPC即可调用控制台中的函数了,不需要再本地还原。

我们来看看实现

1. 先打开客户端,然后打开控制台,将JSrpc的注入代码输入

先输入函数内容

function Hlclient(wsURL) {
    this.wsURL = wsURL;
    this.handlers = {
        _execjs: function (resolve, param) {
            var res = eval(param)
            if (!res) {
                resolve("没有返回值")
            } else {
                resolve(res)
            }

        }
    };
    this.socket = undefined;
    if (!wsURL) {
        throw new Error('wsURL can not be empty!!')
    }
    this.connect()
}

Hlclient.prototype.connect = function () {
    console.log('begin of connect to wsURL: ' + this.wsURL);
    var _this = this;
    try {
        this.socket = new WebSocket(this.wsURL);
        this.socket.onmessage = function (e) {
            _this.handlerRequest(e.data)
        }
    } catch (e) {
        console.log("connection failed,reconnect after 10s");
        setTimeout(function () {
            _this.connect()
        }, 10000)
    }
    this.socket.onclose = function () {
        console.log('rpc已关闭');
        setTimeout(function () {
            _this.connect()
        }, 10000)
    }
    this.socket.addEventListener('open', (event) => {
        console.log("rpc连接成功");
    });
    this.socket.addEventListener('error', (event) => {
        console.error('rpc连接出错,请检查是否打开服务端:', event.error);
    });

};
Hlclient.prototype.send = function (msg) {
    this.socket.send(msg)
}

Hlclient.prototype.regAction = function (func_name, func) {
    if (typeof func_name !== 'string') {
        throw new Error("an func_name must be string");
    }
    if (typeof func !== 'function') {
        throw new Error("must be function");
    }
    console.log("register func_name: " + func_name);
    this.handlers[func_name] = func;
    return true

}

//收到消息后这里处理,
Hlclient.prototype.handlerRequest = function (requestJson) {
    var _this = this;
    try {
        var result = JSON.parse(requestJson)
    } catch (error) {
        console.log("catch error", requestJson);
        result = transjson(requestJson)
    }
    //console.log(result)
    if (!result['action']) {
        this.sendResult('', 'need request param {action}');
        return
    }
    var action = result["action"]
    var theHandler = this.handlers[action];
    if (!theHandler) {
        this.sendResult(action, 'action not found');
        return
    }
    try {
        if (!result["param"]) {
            theHandler(function (response) {
                _this.sendResult(action, response);
            })
            return
        }
        var param = result["param"]
        try {
            param = JSON.parse(param)
        } catch (e) {}
        theHandler(function (response) {
            _this.sendResult(action, response);
        }, param)

    } catch (e) {
        console.log("error: " + e);
        _this.sendResult(action, e);
    }
}

Hlclient.prototype.sendResult = function (action, e) {
    if (typeof e === 'object' && e !== null) {
        try {
            e = JSON.stringify(e)
        } catch (v) {
            console.log(v)//不是json无需操作
        }
    }
    this.send(action + atob("aGxeX14") + e);
}

function transjson(formdata) {
    var regex = /"action":(?<actionName>.*?),/g
    var actionName = regex.exec(formdata).groups.actionName
    stringfystring = formdata.match(/{..data..:.*..\w+..:\s...*?..}/g).pop()
    stringfystring = stringfystring.replace(/\\"/g, '"')
    paramstring = JSON.parse(stringfystring)
    tens = `{"action":` + actionName + `,"param":{}}`
    tjson = JSON.parse(tens)
    tjson.param = paramstring
    return tjson
}

然后再输入

var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz");

其中变量名demo, 和group的值可以自己定

2. 记录加密函数

首先还是调试到加密那一步

这里我们就知道了, 加密函数为l()

在控制台中输入window.enc = l, 控制台会显示当前函数信息, 并保存非形参的参数, 注册成功后我们可以主动调用enc()函数, 查看是否有效

window.enc() = l

#测试
enc("123")

3. 向JsRPC中注册这些函数

#有参
demo.regAction("enc", function (resolve, param) {
    var res = enc(String(param));
    resolve(res);
})
4. 测试调用

发送get请求, 或post请求到本机的12080端口, 即可获取调用结果

http://127.0.0.1:12080/go?group=zzz&action=enc&param=123

懂得原理后, 我们可以继续进行操作了

JS-RPC + MITM

目前比较流行的一个解决方案, 通过 mitm 将原始请求发送到JS-RPC中进行加密后修改原始数据包内容, 再进行发包

mitmproxy 为一款代理工具, 你可以把他理解为python版的burpsuite, 可以进行拦截,改包等操作, 所以我们的思路是这样:

接下来就到实际应用的阶段了:

​ 针对目前的靶场, 我们需要分析一下JS的代码。

几个关键的变量和函数:

  • r: 很明显就是时间戳。
  • n: 原始的表单数据请求经过v() 函数处理后, 再进行JSON编码。
  • i: 使用p函数生成的requestId。
  • s: 使用MD5()函数生成的哈希值, 生成的方式为n+i+r的字符串拼接。
  • 加密: 对变量n使用l()函数进行加密。

针对实际请求包的修改:

我们需要在请求头中添加 timestamp,requestId, sign 等字段

然后修改明文请求体进行加密。

接下来就是实现:

1. 启动JS-rpc, 并注入代码

2. 打上断点并调试, 记录函数, 并注册

记录

//时间戳
window.time = Date.parse
//requestId
window.id = p
//v函数
window.v1 = v
//签名
window.m = a.a.MD5
//加密
window.enc = l

注册

//md5函数
demo.regAction("req", function (resolve,param) {
    //请求头
    let timestamp = time(new Date());
    let requestid = id();
    let v_data = JSON.stringify(v1(param));
    let sign = m(v_data + requestid + timestamp).toString();
    //加密请求体
    let encstr = enc(v_data);

    let res = {
        "timestamp":timestamp,
        "requestid":requestid,
        "encstr":encstr,
        "sign":sign
    };
    resolve(res);
})

测试

这样我们就可以一次性获取所有请求的需求了

3. 构建MITM

​ 之前介绍过Mitmproxy , 就是python版的burpsuite, 所以我们只需要知道核心的代码逻辑: 即提取原始请求体后, 向请求头中添加requestId, timestamp, sign字段 并且 替换原始请求体为加密后的内容就OK了, 直接Chatgpt生成

​ 代码:

import json
import time
import hashlib
import uuid
from mitmproxy import http
import requests
import requests


def request(flow: http.HTTPFlow) -> None:
    if flow.request.pretty_url.startswith("http://39.98.108.20:8085/api/"):
        # 提取原始请求体
        original_body = flow.request.content.decode('utf-8')
        data = {"group": "zzz", "action": "req", "param": original_body}
        res = requests.post("http://127.0.0.1:12080/go",data=data)
        res_json = json.loads(res.text)["data"]
        data_json = json.loads(res_json)
        print(data_json)
        # 对请求体进行加密处理(这里假设加密方法是简单的哈希)
        encrypted_body = data_json["encstr"]

        # 替换请求体
        flow.request.text = encrypted_body

        # 生成 requestId,sign 和 timestamp
        request_id = data_json["requestid"]
        timestamp = data_json["timestamp"]
        sign = data_json["sign"]

        # 添加或替换请求头
        flow.request.headers["requestId"] = request_id
        flow.request.headers["timestamp"] = str(timestamp)
        flow.request.headers["sign"] = sign

# 运行 mitmproxy 时加载这个脚本:mitmproxy -s your_script.py
例:
mitmproxy -p 8083 -s mitm.py

将代码运行起来后, burpsuite 的upstream 设为 mitm的监听端口

4. 测试

在burpsuite中发送明文数据包, 在经过mitm处理后, 自动加密, 此时服务端再不会报错了

JS-RPC + YAKIT 热加载

在刚刚的例子里面, 我们虽然可以实现加解密, 但是毕竟数据包拐了山路十八弯, 难免优点麻烦。 有没有少拐点弯的方法呢? 当然有啦, yakit作为国内优秀的渗透一体化工具,现在的在渗透中的使用率越来越高,相信随着国产化的普及,以后会更加流行。 还不会使用yakit的同学真的可以好好学习一下, 有的功能挺好用的。 在yakit中有一个模块叫做“web fuzzer“,有点像burpsuite中 repeater 和 intruder的结合体, 提供了数据包的重放和fuzz功能。

热加载

​ 通过web fuzzer自带热加载功能, 通过官方对热加载的描述, 我们可以构建一段代码,在发送后自动加密, 这样就省去mitm的使用了。

yak官网对这个热加载功能解释并不是特别详细,仅仅提到了热加载中自带了两个魔术方法, 分别对请求和响应自动做处理

八、热加载 | Yak Program Language (yaklang.com)

1. 原理

通过研究, 可以详细解释解释:

请求包处理: 实现beforeRequest..方法即可, 其中行参”req“为一个字节数组, 保存了完整的请求内容字节。

那么我们通过yak官方的poc库(实际就是HTTP库),提供的方法,可以做如下操作:

//获取请求体
requestBody = poc.GetHTTPPacketBody(req)
//修改请求包中指定的请求头
req = poc.ReplaceHTTPPacketHeader(req, "请求头名", "请求头值")
//修改请求体
req = poc.ReplaceHTTPPacketBody(req, "修改后的值")
2. 实现

首先我们需要准备好, 解密后的请求体, 可以直接把之前提到的变量n的值拿过来

完整热加载内容(JsRPC沿用上面的内容

// 定义加密函数
func getEnc(data){
    rsp,rep,err = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group=zzz&action=req&param="+data, false),poc.appendHeader("content-type", "application/x-www-form-urlencoded"))
    if(err){
        return(err)
    }

    return json.loads(rsp.GetBody())["data"]
}

// beforeRequest 允许发送数据包前再做一次处理,定义为 func(origin []byte) []byte
beforeRequest = func(req) {
    //获取请求体
    req_body = poc.GetHTTPPacketBody(req)
    //加密
    res = getEnc(string(req_body))
    //获取其他的参数
    res = json.loads(res)

    //修改其他的请求头
    req = poc.ReplaceHTTPPacketHeader(req, "requestId", res["requestid"])
    req = poc.ReplaceHTTPPacketHeader(req, "timestamp", res["timestamp"])
    req = poc.ReplaceHTTPPacketHeader(req, "sign", res["sign"])

    //修改请求体
    req = poc.ReplaceHTTPPacketBody(req, res["encstr"])


    return []byte(req)
}

// afterRequest 允许对每一个请求的响应做处理,定义为 func(origin []byte) []byte
afterRequest = func(rsp) {
    return []byte(rsp)
}

// mirrorHTTPFlow 允许对每一个请求的响应做处理,定义为 func(req []byte, rsp []byte, params map[string]any) map[string]any
// 返回值回作为下一个请求的参数,或者提取的数据,如果你需要解密响应内容,在这里操作是最合适的
mirrorHTTPFlow = func(req, rsp, params) {
    return params
}

最后通过fuzz功能测试暴力破解,爆破成功

​ 当然在这个过程中也并不是一帆风顺, 最大的一个问题还是yakit官方文档对热加载功能和poc库的解释还不够详细,导致很多功能都是靠猜(小小的吐槽一下)。但是该有的功能其实都有,工具本身还是很强大的。

yakit解密

​ 关于解密,其实和加密一样,如果懂得前面的逻辑,完全可以实现自动化,这里就不具体介绍了。

​ 但除此之外,我们还可以使用yak中新增插件的方式来手动解密数据。原理也非常简单,我们来看看:

1. 还是在控制台中记录和注册解密函数

//记录:这里解密函数就是d函数
window.dec = d
//注册
demo.regAction("decrypt", function (resolve,param) {
    resolve(dec(param))
})

2. 插件–本地–新建插件

类型选择codec插件, 勾选右键执行

3. 编辑内容–保存

handle = func(origin /*string*/) {
    //JSrpc的group
    group = "zzz";
    //jsrpc的action
    action = "decrypt";
    rsp,rep = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group="+group+"&action="+action+"&param="+codec.EncodeUrl(origin), false),poc.appendHeader("content-type", "application/x-www-form-urlencoded"))~

    return json.loads(rsp.GetBody())["data"];
}

可以做个测试

4. 在web_fuzzer中使用

当然肯定有人问, burpsuite怎么办呢?当然也有办法,比如autodecoder插件就可以完成,但实现比这个还是要麻烦得多。

JS原生

JS解密的最终形态是什么呢?爬虫高手会告诉你,当然是用JS解密JS啊,通过反混淆–分析代码逻辑–研究参数作用–添加补环境–最终通过本地运行JS来加解密。但是我们之前也讨论过,等这一套操作搞完,可能渗透测试项目都快结束了, 所以为了速度,我们只能用魔法打败魔法。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇