某webvpn系统的编码分析及任意网站访问的实现

webvpn用于校外以学校IP访问文献资源,本文分析并构造某系统的URL编码方案,以求高效访问指定资源

高校通过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解出来是:

1
2
3
www.cnki.net
2
a458dd05

其中,2应该是文献平台id,a458dd05应该是某种加盐。简单情况下它是一个固定值,复杂的可能是在服务端根据身份和时间进行计算。通过更换浏览器等方式,发现应该是由固定字符串加盐得到。

而又访问cnki的不同链接,发现编码结果不变,因此说明其只对domain部分进行加盐。

0x02 网站前端调试分析

通过该链接访问后会被重定向到一个编码的链接。通过F12观察请求和源码,不难发现服务器返回的html内容的所有链接均已被编码,同时通过html文件引入了两个脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE
 html>
<html lang="zh">
<script>
var __wras_worker=false;
var __wras_docType=0;
var __wras_env_sid="(an env sid)";
</script>
<script src="/rwt/initEnvSid?id=2&amp;t=(a time stamp)"></script>
<script src="/js/wrasutils.js" charset="utf-8"></script>

其中第一个脚本的id正是该文献平台在入口处的id,而t是一个时间戳。该该文件的结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
self['__wras_access_token'] = '(a token)';
self['__wras_user_config'] = {
    "monitorConfig": [{
        "action": "visit",
        "url": "http://www.cnki.net/"
    }],
    "statsConfig": [{
        "stage": 1,
        "urlMatched": false,
        "description": "首页检索",
        "fields": [{
            "fieldKey": 2,
            "placeholder": "中文文献、外文文献",
            "target": "//*[@id=\"txt_SearchText\"]"
        }],
        "url": "https://www.cnki.net/",
        "events": [{
            "event": "click",
            "target": "//*[@class=\"search-btn\"]"
        }]
    },

第二个脚本是一个长达750kb的js,进行了复杂的混淆,且使用了env_sidaccess_tokenstatsConfig等变量。最复杂的情况下,它可能会利用前两个变量进行加盐,还会按照statsConfig的规则对编码URL设置白名单以防止篡改或劫持。

0x03 任意访问初步设计

我们希望最终能通过脚本/网页前端辅助实现任意网站访问。一般有三种构造方案:

  1. 通过在文献网站中构造可以点击并被编码的链接,从网站内直接跳转,甚至可以不需要插件。其原理等价于“用儿童手表启动原神”;
  2. 通过分析并调用加密脚本中的指定函数,或者劫持脚本并控制参数输入输出直接由脚本完成加密,可选动态劫持或静态劫持;
  3. 通过分析加密算法,复刻一个一样加密函数出来。

方案一可以抵御大多数脚本加密逻辑的变化,产生稳定的结果。然而找到这样的网址并不容易,而且观察发现部分超链接甚至不会被编码。

方案二需要对加密脚本进行大量分析,但是该脚本非常复杂,且使用了很多匿名函数和闭包,使得很多属性在运行时已经无法被提取了,几乎无法动态劫持。而静态劫持要求在该脚本加载之前进行加载:由于该脚本通过HTML引入,篡改猴加载时机太晚,浏览器插件由于在Manifest V3中webRequestBlocking不再可用而很难实现对fetch的劫持。这种方案可以应对一些程度的加密算法修改,但是可能面临更复杂的混淆挑战。

方案三需要分析加密算法。观察到脚本其实可以实现更复杂的加密保护机制,但是可能出于服务器计算开销等的考虑,而只使用了简单的MD5+base62,最后我们通过这种方案实现了任意网站访问。

0x04 加密脚本分析

首先考虑静态分析。

用IDE进行格式化增强可读性,然后观察组成:脚本主体由一个括号构造的数组和两个全局函数组成。全局函数起到字典作用,防止暴露脚本内代码意图。括号内包含两个匿名函数,都是定义之后立即传入参数调用,例如function(...args){}(),第二个函数调用甚至是本体作为参数传递给另一个函数调用。

用AI对脚本进行分析,定位到改写URL的函数位于_0x3b5f14['rewrite'] = (_0x1bfebd, _0x348c22) => {...}。此外,对一些字符串进行搜索,发现其使用了外部环境变量,并且实现了一个栈重写了函数调用和传递参数的逻辑(修改了definePropertytoString等函数,尚不确定是否使用栈保护执行流程)。再搜索有关调试的字符串,观察事件监听器,未找到对调试行为的阻止。在字符串中也没找到有关上报攻击行为的代码,因而可以放心调试。

随后开始动态分析,先对rewrite函数打断点,然后单步步进跟踪其参数逻辑,发现其第一个参数是要编码的URL,第二个参数是URL的类型,如script或xhr等,可能和请求类型有关。往下看发现其将URL分成了三部分:

1
2
3
4
5
{
    'protocol': _0x313b6b,
    'authority': _0x56a489,
    'file': _0x2361fd
};

返回的时候先组了个字符串,再尾调用另一个函数对其进行处理。

1
2
_0x13947c = _0x5f0573 + '/' + _0x1c2f1c[_0xd17f7e(0x597)] + '/' + _0x3b5f14['encodeHost'](_0x1c2f1c[_0xd17f7e(0x3c0)]) + _0x1c2f1c['file'];
return _0x4dc7cc(_0x13947c, _0x348c22, _0x304f26);

这里面有一个encodeHost应该是关键。跟踪之:

1
2
3
_0x426dd6 = _0x545ff8[_0x346a6b(0x654)](_0x545ff8[_0x346a6b(0x385)](_0x426dd6), '\x0a', '');
const _0x24411b = _0xc29ce8[_0x346a6b(0x6de)][_0x346a6b(0x20b)](_0x426dd6 + _0x2afab9 + _0x1d1fdd);
return _0x3c97a4[_0x346a6b(0x15e)](_0x426dd6 + '\x0a' + _0x2afab9 + '\x0a' + _0x24411b[_0x346a6b(0x7ef)](0x0, 0x8));

此时已经和之前对编码的猜测八九不离十了,第三行的8个字符就是对host加盐再取md5的前8位。_0x346a6b(0x6de)对应的字符串是"Md5",而_0x346a6b(0x15e)对应的字符串是"encode"。此时在动态调试环境内已经可以看到_0x426dd6是要编码的host(前面的authority部分),_0x2afab9 + _0x1d1fdd是固定的加盐字符串,已经可以复刻加盐算法。立即对字符串进行md5计算确定了这一结论。

此后跟进调试再观察,内部是一个非常复杂的Md5算法和base62编码算法(最初看到里面由states变量还以为编码是由状态的,结果可能也只是混淆的结果)。此时已经基本完成了编码,尾调用函数做一些其它的处理。

考虑到加密算法可能发生变化,我尝试进行了劫持,但是都以失败告终,比较可行的还是静态劫持,例如浏览器劫持请求、抓包等,但是难以写进脚本做成工具。

首先尝试劫持加密函数,即使用篡改猴脚本。尽管不能在执行加密脚本之前劫持函数定义,但是可以后续分析函数调用传参等,保存函数的引用,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function interceptFunction() {
    const originalDefineProperty = Object.defineProperty;
    Object.defineProperty = function(obj, prop, descriptor) {
        if (descriptor && typeof descriptor.value === 'function') {
            const originalFunc = descriptor.value;
            descriptor.value = function(...args) {
                if (args.length >= 2 && 
                    typeof args[0] === 'string' && 
                    args[0].startsWith('https://piccache.cnki.net/') && 
                    args[1] === ';wras_type_img') {
                    interceptedFunc = originalFunc.bind(this);
                }
                return originalFunc.apply(this, args);
            };
        }
        return originalDefineProperty.call(Object, obj, prop, descriptor);
    };
}

但是加密脚本预先劫持了函数定义等劫持依赖的功能,修改了传参方式,因而避免了这种劫持方式。而其它涉及编码部分的逻辑都非常深,难以运行时定位:

复杂的函数和复杂的栈

而考虑静态劫持的思路是,对页面发出的所有请求在插件内的background.js进行过滤,并且修改响应体再返回,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
chrome.webRequest.onBeforeRequest.addListener(
    function(details) {
        if (details.url.includes('wrasutils.js')) {
        return new Promise((resolve) => {
            fetch(details.url)
            .then(response => response.text())
            .then(text => {
                const modifiedText = text.replace(
                /_0x4dc7cc\(_0x13947c,\s*_0x348c22,\s*_0x304f26\)/g,
                'console.log(_0x13947c),_0x4dc7cc(_0x13947c,_0x348c22,_0x304f26)'
                );
                const blob = new Blob([modifiedText], { type: 'application/javascript' });
                resolve({
                redirectUrl: URL.createObjectURL(blob)
                });
            });
        });
        }
    },
    { urls: ["<all_urls>"] },
    ["blocking"]
); 

但是该API只在Manifest V2之前的版本可用(2024之后被废弃),如果要强制启用则需要用户进行侵入式的修改(需要修改注册表),很不方便。

0x05 任意网站访问构造

因此,最后在UCAS Web Helper中复刻了加密算法来实现webvpn的任意网站访问,而放弃采用劫持获取编码链接的方案,其关键代码如下:

1
2
3
const encodingInput = authority + '\n2\n' + CryptoJS.MD5(authority + "(a certain string)").toString().substring(0, 8);
const encodedAuthority = base62Encode(encodingInput);
return `https://(a certain website)/${protocol}/${encodedAuthority}${file}`

不过这种方法不抵御加密算法的改变。如果各位有基于劫持的方法或其它更好的实现方案,欢迎留言交流。

Licensed under CC BY-NC-SA 4.0
Last updated on Jan 28, 2025 03:48 CST
Total Page View: Loading  Total Visits: Loading  Site Total Visitors: Loading
京ICP备2024091870号-1
Built with Hugo
Theme Stack designed by Jimmy