高校通过webvpn实现校园外访问校内/图书馆内资源。wenvpn通过对URL进行编码,将资源重定向到校内主机上,由主机使用学校指定的出口IP代为访问,从而完成机构认证。
为了确保原网站的所有资源在没有代理的情况下通过这种方式访问,webvpn系统需要编码所有资源链接,包括html/js/css/img等类型。其中,js和css类型的文件需要获得响应体之后进行进一步处理,比如编码链接、去除一致性校验等。
而为降低服务端压力以及降低被攻击的概率,仅html部分在服务端编码,而对js和css的解析放在客户端进行,这为我们构造URL提供了机会。
本文介绍了对某webvpn的URL编码分析,并给出实现任意资源访问的思路。
本文仅供学习交流,禁止任何滥用行为。
0x01 URL编码初步分析
首先观察webvpn的入口,该系统不能直接通过URL登录(没有正确的账号/密码),而是需要通过一个平台跳转。根据Headers分析,平台会向入口传递一个正确的Cookie,从而完成身份鉴权。
入口处会提供其它文献平台入口,而该链接是没有进行编码的,仅通过id区分平台。
我们欲访问的网站是www.cnki.net
,返回的编码结果是JLxYOF1Zz9HGSwUxtPp8lRAps7uAPZ7
,对应访问的资源是https://webvpn_domain/https/JLxYOF1Zz9HGSwUxtPp8lRAps7uAPZ7/
。
根据编码,第一次推测是base64,然而发现解不出来。观察到这种编码并没有引入=
或其它补位/特殊字符,遂推测依然是base*编码。根据尝试,得出是base62编码。
该编码用base62解出来是:
|
|
其中,2
应该是文献平台id,a458dd05
应该是某种加盐。简单情况下它是一个固定值,复杂的可能是在服务端根据身份和时间进行计算。通过更换浏览器等方式,发现应该是由固定字符串加盐得到。
而又访问cnki的不同链接,发现编码结果不变,因此说明其只对domain部分进行加盐。
0x02 网站前端调试分析
通过该链接访问后会被重定向到一个编码的链接。通过F12观察请求和源码,不难发现服务器返回的html内容的所有链接均已被编码,同时通过html文件引入了两个脚本:
|
|
其中第一个脚本的id正是该文献平台在入口处的id,而t是一个时间戳。该该文件的结构如下:
|
|
第二个脚本是一个长达750kb的js,进行了复杂的混淆,且使用了env_sid
、access_token
和statsConfig
等变量。最复杂的情况下,它可能会利用前两个变量进行加盐,还会按照statsConfig的规则对编码URL设置白名单以防止篡改或劫持。
0x03 任意访问初步设计
我们希望最终能通过脚本/网页前端辅助实现任意网站访问。一般有三种构造方案:
- 通过在文献网站中构造可以点击并被编码的链接,从网站内直接跳转,甚至可以不需要插件。其原理等价于“用儿童手表启动原神”;
- 通过分析并调用加密脚本中的指定函数,或者劫持脚本并控制参数输入输出直接由脚本完成加密,可选动态劫持或静态劫持;
- 通过分析加密算法,复刻一个一样加密函数出来。
方案一可以抵御大多数脚本加密逻辑的变化,产生稳定的结果。然而找到这样的网址并不容易,而且观察发现部分超链接甚至不会被编码。
方案二需要对加密脚本进行大量分析,但是该脚本非常复杂,且使用了很多匿名函数和闭包,使得很多属性在运行时已经无法被提取了,几乎无法动态劫持。而静态劫持要求在该脚本加载之前进行加载:由于该脚本通过HTML引入,篡改猴加载时机太晚,浏览器插件由于在Manifest V3中webRequestBlocking不再可用而很难实现对fetch的劫持。这种方案可以应对一些程度的加密算法修改,但是可能面临更复杂的混淆挑战。
方案三需要分析加密算法。观察到脚本其实可以实现更复杂的加密保护机制,但是可能出于服务器计算开销等的考虑,而只使用了简单的MD5+base62,最后我们通过这种方案实现了任意网站访问。
0x04 加密脚本分析
首先考虑静态分析。
用IDE进行格式化增强可读性,然后观察组成:脚本主体由一个括号构造的数组和两个全局函数组成。全局函数起到字典作用,防止暴露脚本内代码意图。括号内包含两个匿名函数,都是定义之后立即传入参数调用,例如function(...args){}()
,第二个函数调用甚至是本体作为参数传递给另一个函数调用。
用AI对脚本进行分析,定位到改写URL的函数位于_0x3b5f14['rewrite'] = (_0x1bfebd, _0x348c22) => {...}
。此外,对一些字符串进行搜索,发现其使用了外部环境变量,并且实现了一个栈重写了函数调用和传递参数的逻辑(修改了defineProperty
和toString
等函数,尚不确定是否使用栈保护执行流程)。再搜索有关调试的字符串,观察事件监听器,未找到对调试行为的阻止。在字符串中也没找到有关上报攻击行为的代码,因而可以放心调试。
随后开始动态分析,先对rewrite
函数打断点,然后单步步进跟踪其参数逻辑,发现其第一个参数是要编码的URL,第二个参数是URL的类型,如script或xhr等,可能和请求类型有关。往下看发现其将URL分成了三部分:
|
|
返回的时候先组了个字符串,再尾调用另一个函数对其进行处理。
|
|
这里面有一个encodeHost
应该是关键。跟踪之:
|
|
此时已经和之前对编码的猜测八九不离十了,第三行的8个字符就是对host加盐再取md5的前8位。_0x346a6b(0x6de)
对应的字符串是"Md5",而_0x346a6b(0x15e)
对应的字符串是"encode"。此时在动态调试环境内已经可以看到_0x426dd6
是要编码的host(前面的authority部分),_0x2afab9 + _0x1d1fdd
是固定的加盐字符串,已经可以复刻加盐算法。立即对字符串进行md5计算确定了这一结论。
此后跟进调试再观察,内部是一个非常复杂的Md5算法和base62编码算法(最初看到里面由states变量还以为编码是由状态的,结果可能也只是混淆的结果)。此时已经基本完成了编码,尾调用函数做一些其它的处理。
考虑到加密算法可能发生变化,我尝试进行了劫持,但是都以失败告终,比较可行的还是静态劫持,例如浏览器劫持请求、抓包等,但是难以写进脚本做成工具。
首先尝试劫持加密函数,即使用篡改猴脚本。尽管不能在执行加密脚本之前劫持函数定义,但是可以后续分析函数调用传参等,保存函数的引用,例如:
|
|
但是加密脚本预先劫持了函数定义等劫持依赖的功能,修改了传参方式,因而避免了这种劫持方式。而其它涉及编码部分的逻辑都非常深,难以运行时定位:
而考虑静态劫持的思路是,对页面发出的所有请求在插件内的background.js进行过滤,并且修改响应体再返回,例如:
|
|
但是该API只在Manifest V2之前的版本可用(2024之后被废弃),如果要强制启用则需要用户进行侵入式的修改(需要修改注册表),很不方便。
0x05 任意网站访问构造
因此,最后在UCAS Web Helper中复刻了加密算法来实现webvpn的任意网站访问,而放弃采用劫持获取编码链接的方案,其关键代码如下:
|
|
不过这种方法不抵御加密算法的改变。如果各位有基于劫持的方法或其它更好的实现方案,欢迎留言交流。