一、简介
对于常见的web或者h5的场景中,一些重要的系统都会对于参数或者敏感数据进行校验,防止被恶意篡改而利用。因此在安全测试过程中,对于一些越权、注入等测试,需要对参数值进行修改重放,那么这个过程是否能够成功第一步取决于是否可以搞定校验算法并成功绕过。因此本文主要介绍安全测试中常用的一些js逆向的技术手段。
当然对于web场景中安全测试会遇到的一些校验场景,一般包括如下两类:
1)请求参数签名验证
2)请求、响应内容加密
二、常见浏览器调试方法
js中校验算法的查找定位以及逆向分析,需要对浏览器中的部分功能模块熟练掌握,接下来对其中的面板功能、断点调试方法以及断点天机方法进行介绍。
2.1 浏览器面板功能
chrome浏览器中的面板包括元素、控制台、源代码、网络、性能、内存以及应用等多个。重点介绍常见面板功能。
元素(Elements): 可以查看DOM结构、编辑CSS样式,用于测试页面布局和设计页面;
控制台(Console):执行JavaScript脚本,也可以通过Console和页面中的Javascript对象交互;
源代码(Sources):查看Web应用加载的所有文件;编辑CSS和JavaScript文件内容,包括如下:(page : 所有资源文件;filesystem: 关联本地文件;overrides: 可以做文件替换,比如替换JS;代码段:可以编写脚本,影响页面,代码记录);
网络(Network):展示页面中所有的请求内容列表,能查看每项的请求头、请求行、请求体、时间线以及网络请求的瀑布图等信息;
性能(Performance):分析网页的性能表现,包括加载时间,CPU使用率、内存占用等。可以录制网页的运行过程,进行详细的性能分析;
内存(Memory):用于记录和分析页面占用内存的情况;
应用(Application):查看Web应用的数据存储情况;IndexedDB;Web SQL;本地和会话存储;Cookie;应用程序缓存图像字体和样式表等。
2.2 断点添加方式
js逆向分析中有一个非常重要的功能需要掌握,就是下断点,接下来对常用断点方式进行介绍。
1)点击关键代码进行进行断点:通过点击代码便可以下断点
2)XHR断点:执行比较靠后,距离加密函数相对较近,可以根据栈快速定位
点击+号后直接输入需要定位的接口地址即可
3)DOM事件断点:执行的比较靠前,距离加密函数比较远
在Chrome开发者工具的Elements标签页中,找到你想要设置断点的DOM元素,右键点击该元素,选择"Break on"选项,然后根据需要选择以下三种断点之一:
Subtree Modifications:当该节点的子树发生变化时触发断点。
Attributes Modifications:当该节点的属性发生变化时触发断点。
Node Removal:当该节点被移除时触发断点
2.3 断点调试方法
通过设置断点,运行程序后就会断在设置好的断点处,这时就需要跟进代码执行,分析代码和数据的变化,梳理判断出校验的算法和逻辑,好进行利用。
图中红框依次功能如下:
跳过子函数(次态函数)执行(只在主函数内一步一步执行,不进入子函数内部)
进入子函数(次态函数)执行(在主函数内部一步一步执行,如果遇到子函数,会跳转到子函数内部一步一步执行)
跳出当前函数,回到调用位置
单步执行,会进入到函数内部 更加的细致
屏蔽断点
三、js逆向分析实战
了解了一些基础的调试操作内容,接下来针对js的逆向分析进行实操,本次主要以请求参数的签名验证和响应内容的加密两方面进行分析和绕过实战,这也是平时安全测试过程中最常见的两种场景。
3.1 请求参数签名验证实战
通过抓包可以看到该请求体中存在sign参数,值为一段数字字母
修改page内容,重新进行提交请求,返回错误,那么这时候想要测试注入等手法都是无法成功的,因为当重放是sign值是未变的,所以后端验证也是不通过的。接下来首先就需要找出sign的算法逻辑,尝试绕过后才可以进行测试。
3.1.1 算法分析
通过数据包分析判断为sign参数为参数校验,因此在调试解密直接搜索关键词sign=,成功在代码中查询到,
可以看到sign=Xt(a)这个方法,在此处点击下断点
通过断点可以看到Xt()方法中传入了n,n为一系列用户端数据和key值生成,
继续进入下一个函数调用,可以看到为Vt(),并传入了上边的t作为参数
查看Vt()方法实际名称为Et
进入Et函数,发现分别执行Rt->At->Tt三个方法,将最终执行完的结果返回。
三个方法具体内容也是在一起,依次进行执行,执行完Rt进入At执行
可以看到At中又包括Lt->Nt->Dt三个函数
依次进行下一个函数进行执行
进入Lt函数执行,可以发现大量循环操作等执行,如果没有太多意义可以跳出当前函数,进行下一个函数的执行
通用的执行方法进行Nt和Dt两个函数的执行和分析
当At函数执行完,可以看到返回了一串值
再进入Tt中执行
可以看出其实是一个字符串的对比操作
继续执行完Et后可以看出返回了遗传字符,应该就是我们的sign的值
继续跟进,可以看到对于这段值有做了一次大写的操作
完成后进行了返回
那这个时候也就分析完成了,我们可以确认Xt函数将入参执行完成后就会返回为sign用来做校验
在控制台获取传入的n值,并打印Xt(n)方法,额可以看到成功打印出我们需要的sign值
3.1.2 算法逆向绕过实战
通过上述的分析,可以看出该程序中的sign的获取是通过Xt()函数传入n值进行生成的,n为一个固定格式的用户的参数,那么便可以通过修改n的值生成sign来绕过校验了,接下来我们通过三种方法进行实操。
3.1.2.1 浏览器校验调用
抓包获取到详细数据包
修改参数重放失败
在获取到的n中将对应的参数page的值进行修改,打印Xt(n)生成一个新的sign
修改burp中的sign进行重放,成功获得数据,那么接下来便可以进行各类payload的参数测试了。
3.1.2.2 在线算法生成
通过该sign的值内容和长度可以初步判断出为md5加密所得,及Xt()方法为一个md5算法,因此利用在线md5平台,传入n的值,修改page参数重新生成大写的sign值
修改burp中的sign,成功重放
3.1.2.3 js代码补环境复用
通过算法分析可以看出主要调用Et()方法后进行一系列的函数调用,最后返回了需要的sign值,那么这时我们将该js中调用执行过的方法进行复制,在本地新建一个js,粘贴其中,最终打印Et()方法,Et中传入需要的值,也就是n,然后进行运行,如果存在报错缺少方法,缺哪个便去js中复制那个。
完成后执行该js文件,发现成功生成需要的值。
具体代码如下:
css
var St = 0; function Et(s) { return Tt(At(Rt(s))) } function At(s) { return Dt(Nt(Lt(s), 8 * s.length)) } function Tt(input) { for (var t, e = St ? "0123456789ABCDEF" :"0123456789abcdef", output = "", i = 0; i < input.length; i++) t = input.charCodeAt(i), output += e.charAt(t >>> 4 & 15) + e.charAt(15 & t); return output } function Rt(input) { for (var t, e, output = "", i = -1; ++i < input.length; ) t = input.charCodeAt(i), e = i + 1 < input.length ? input.charCodeAt(i + 1) : 0, 55296 <= t && t <= 56319 && 56320 <= e && e <= 57343 &&(t = 65536 + ((1023 & t) << 10) + (1023 & e), i++), t <= 127 ? output += String.fromCharCode(t) : t <= 2047 ?output += String.fromCharCode(192 | t >>> 6 & 31, 128 | 63 & t) : t<= 65535 ? output += String.fromCharCode(224 | t >>> 12 & 15, 128| t >>> 6 & 63, 128 | 63 & t) : t <= 2097151 && (output +=String.fromCharCode(240 | t >>> 18 & 7, 128 | t >>> 12 & 63, 128| t >>> 6 & 63, 128 | 63 & t)); return output } function Lt(input) { for (var output = Array(input.length >> 2), i = 0; i <output.length; i++) output[i] = 0; for (i = 0; i < 8 * input.length; i += 8) output[i >> 5] |= (255 & input.charCodeAt(i / 8)) <<i % 32; return output } function Dt(input) { for (var output = "", i = 0; i < 32 * input.length; i += 8) output += String.fromCharCode(input[i >> 5] >>> i %32 & 255); return output } function Nt(t, e) { t[e >> 5] |= 128 << e % 32, t[14 + (e + 64 >>> 9 << 4)] = e; for (var a = 1732584193, b = -271733879, n = -1732584194, r =271733878, i = 0; i < t.length; i += 16) { var o = a , c = b , l = n , f = r; a = qt(a, b, n, r, t[i + 0], 7, -680876936), r = qt(r, a, b, n, t[i + 1], 12, -389564586), n = qt(n, r, a, b, t[i + 2], 17, 606105819), b = qt(b, n, r, a, t[i + 3], 22, -1044525330), a = qt(a, b, n, r, t[i + 4], 7, -176418897), r = qt(r, a, b, n, t[i + 5], 12, 1200080426), n = qt(n, r, a, b, t[i + 6], 17, -1473231341), b = qt(b, n, r, a, t[i + 7], 22, -45705983), a = qt(a, b, n, r, t[i + 8], 7, 1770035416), r = qt(r, a, b, n, t[i + 9], 12, -1958414417), n = qt(n, r, a, b, t[i + 10], 17, -42063), b = qt(b, n, r, a, t[i + 11], 22, -1990404162), a = qt(a, b, n, r, t[i + 12], 7, 1804603682), r = qt(r, a, b, n, t[i + 13], 12, -40341101), n = qt(n, r, a, b, t[i + 14], 17, -1502002290), a = Ft(a, b = qt(b, n, r, a, t[i + 15], 22, 1236535329),n, r, t[i + 1], 5, -165796510), r = Ft(r, a, b, n, t[i + 6], 9, -1069501632), n = Ft(n, r, a, b, t[i + 11], 14, 643717713), b = Ft(b, n, r, a, t[i + 0], 20, -373897302), a = Ft(a, b, n, r, t[i + 5], 5, -701558691), r = Ft(r, a, b, n, t[i + 10], 9, 38016083), n = Ft(n, r, a, b, t[i + 15], 14, -660478335), b = Ft(b, n, r, a, t[i + 4], 20, -405537848), a = Ft(a, b, n, r, t[i + 9], 5, 568446438), r = Ft(r, a, b, n, t[i + 14], 9, -1019803690), n = Ft(n, r, a, b, t[i + 3], 14, -187363961), b = Ft(b, n, r, a, t[i + 8], 20, 1163531501), a = Ft(a, b, n, r, t[i + 13], 5, -1444681467), r = Ft(r, a, b, n, t[i + 2], 9, -51403784), n = Ft(n, r, a, b, t[i + 7], 14, 1735328473), a = Ut(a, b = Ft(b, n, r, a, t[i + 12], 20, -1926607734),n, r, t[i + 5], 4, -378558), r = Ut(r, a, b, n, t[i + 8], 11, -2022574463), n = Ut(n, r, a, b, t[i + 11], 16, 1839030562), b = Ut(b, n, r, a, t[i + 14], 23, -35309556), a = Ut(a, b, n, r, t[i + 1], 4, -1530992060), r = Ut(r, a, b, n, t[i + 4], 11, 1272893353), n = Ut(n, r, a, b, t[i + 7], 16, -155497632), b = Ut(b, n, r, a, t[i + 10], 23, -1094730640), a = Ut(a, b, n, r, t[i + 13], 4, 681279174), r = Ut(r, a, b, n, t[i + 0], 11, -358537222), n = Ut(n, r, a, b, t[i + 3], 16, -722521979), b = Ut(b, n, r, a, t[i + 6], 23, 76029189), a = Ut(a, b, n, r, t[i + 9], 4, -640364487), r = Ut(r, a, b, n, t[i + 12], 11, -421815835), n = Ut(n, r, a, b, t[i + 15], 16, 530742520), a = Bt(a, b = Ut(b, n, r, a, t[i + 2], 23, -995338651), n,r, t[i + 0], 6, -198630844), r = Bt(r, a, b, n, t[i + 7], 10, 1126891415), n = Bt(n, r, a, b, t[i + 14], 15, -1416354905), b = Bt(b, n, r, a, t[i + 5], 21, -57434055), a = Bt(a, b, n, r, t[i + 12], 6, 1700485571), r = Bt(r, a, b, n, t[i + 3], 10, -1894986606), n = Bt(n, r, a, b, t[i + 10], 15, -1051523), b = Bt(b, n, r, a, t[i + 1], 21, -2054922799), a = Bt(a, b, n, r, t[i + 8], 6, 1873313359), r = Bt(r, a, b, n, t[i + 15], 10, -30611744), n = Bt(n, r, a, b, t[i + 6], 15, -1560198380), b = Bt(b, n, r, a, t[i + 13], 21, 1309151649), a = Bt(a, b, n, r, t[i + 4], 6, -145523070), r = Bt(r, a, b, n, t[i + 11], 10, -1120210379), n = Bt(n, r, a, b, t[i + 2], 15, 718787259), b = Bt(b, n, r, a, t[i + 9], 21, -343485551), a = zt(a, o), b = zt(b, c), n = zt(n, l), r = zt(r, f) } return Array(a, b, n, r) } function Mt(q, a, b, t, s, e) { return zt((n = zt(zt(a, q), zt(t, e))) << (r = s) | n >>>32 - r, b); var n, r } function qt(a, b, t, e, n, s, r) { return Mt(b & t | ~b & e, a, b, n, s, r) } function Ft(a, b, t, e, n, s, r) { return Mt(b & e | t & ~e, a, b, n, s, r) } function Ut(a, b, t, e, n, s, r) { return Mt(b ^ t ^ e, a, b, n, s, r) } function Bt(a, b, t, e, n, s, r) { return Mt(t ^ (b | ~e), a, b, n, s, r) } function zt(t, e) { var n = (65535 & t) + (65535 & e); return (t >> 16) + (e >> 16) + (n >> 16) << 16 | 65535& n } console.log(Et("test"));
那么这个时候再传入n,及前面获取到的实际字段,并修改page的值重新生成
burp中替换sign重放,成功获取到数据
3.2 请求、响应内容加密
安全测试中,除了参数的校验之外,还有一类就是请求或者响应内容的加密,那么需要测试就必须先解密,看到原始内容才可以进行,本次以响应内容加密作为案例进行分析实操。
3.2.1 算法分析
通过截图可以看到,利用系统可以做翻译,
但是实际接口返回值是加密的,因此需要通过抓包接口测试,必须找到解密算法,获取响应内容
在源代码中进行XHR断点设置,添加uri为断点
输入数据,成功断下,可以看到断到了send函数,这个其实是向服务器发包的一个方法
跳过到下一个函数进行执行
可以看到data为key值等
继续跳过执行,可以看到成功获取到返回的加密值
再跳过执行可以看到执行该函数da.A.decodeData(),从名称也可以看出为一个解码的函数,分别传入了o和key以及iv,那么o为加密的值,key和iv分别为前面获取到的内容,执行完成后成功返回了解密后的明文数据
那么通过控制台面板,出入o为加密值,复制da.A.decodeData()进行执行,成功获得解密的数据。
3.2.2 算法逆向绕过
通过上述算法成功获取到具体的解密函数,这时利用burp抓包,修改参数为各类payload,进行重放
将获取到的加密数据,通过控制台面板传入o,并调用da.A.decodeData()成功获取原始数据,达到测试的效果。
当然,通过该解密方法的传值和分析,其实也可以看出是什么加密算法,也可以直接通过编写本地算法代码进行解密,具体不再详细赘述。
四、总结
对于安全测试或者漏洞挖掘中,这类js的加密校验也是越来越多,目的也是为了达到测试攻击等行为的成本,要想完成测试首先需要分析或者绕过校验参数,对于测试或者漏洞挖掘来说,怎么速度更快更能高效的达到算法的绕过其实目的就达到了。根据上述列出的两类场景和分析实战方法,便可以满足大部分的js逆向场景。