1. 项目概述:从一道CTF真题看JSFuck编码的实战解密
最近复盘了LitCTF 2023的一道Web题目,核心考点是JSFuck编码的识别与解密。这道题本身难度不算太高,但非常典型,它完美地展示了CTF比赛中“编码混淆”类题目的常见套路和解题思路。很多刚接触CTF的朋友,尤其是Web方向的新手,一看到满屏的括号、加号和叹号,可能直接就懵了,不知道从何下手。其实,只要理解了JSFuck的基本原理,并掌握几个高效的在线工具和手动分析方法,这类题目就能迎刃而解,甚至成为你的“送分题”。
JSFuck是一种极端的JavaScript混淆技术,它仅使用六个字符[、]、(、)、+、!来编写任何有效的JavaScript代码。在CTF的Web题目中,出题人经常用它来隐藏关键的验证逻辑、flag生成算法或者简单的提示信息。解题者的任务,就是将这些看似天书的字符组合,还原成可读、可理解的JavaScript代码,从而找到突破口。本文将基于LitCTF 2023的真题,手把手带你走完从“看到一团乱码”到“成功获取flag”的完整流程,重点分享我常用的在线工具、手动解密的小技巧,以及在实战中容易踩的坑。
2. 核心原理:JSFuck是如何“无中生有”的
在动手解密之前,我们有必要花点时间理解JSFuck的“魔法”是如何实现的。这不仅能帮你更好地使用工具,当工具失效时,你还能依靠原理进行手动分析。它的核心思想,是利用JavaScript语言中类型转换和运算符的特性,从“无”中构造出“有”。
2.1 构造逻辑值与数字的基石:false,true,0,1
一切始于两个特殊的表达式:
![]的结果是false。因为空数组[]在布尔上下文中为true,取反后得到false。!![]的结果是true。双重否定,恢复了原始的布尔真值。
有了false和true,我们就可以构造数字了。在JavaScript中,布尔值参与算术运算时会被转换为数字:false转为0,true转为1。
+!![]的结果是数字1。!![]是true,前面的+是一元加运算符,将其强制转换为数字1。+![]的结果是数字0。
2.2 构造任意数字:字符串连接与类型转换
如何得到数字2、3甚至更大的数字呢?这里用到了数组的+运算符进行字符串连接,以及parseInt函数的原理。 例如,构造数字2:
- 我们有
![]是false,其字符串形式是"false"。 ![]+[]的结果是字符串"false"。这里的+[]是将空数组转换为空字符串"",然后与"false"连接,结果仍是"false"。- 字符串
"false"可以通过下标访问字符:(![]+[])[+!![]]。我们来拆解:(![]+[])是"false"。[+!![]]是[1],表示取下标为1的字符。"false"[1]是字母"a"。
- 同理,我们可以构造出字符
"l","s","e",最终拼出完整的字符串"false"和"true"。更重要的是,我们可以拼出"undefined"、"[object Object]"等包含更多字符的字符串。
有了这些字符,就可以拼出像"constructor"这样的关键字符串。然后通过访问数字的toString方法(即(数字)["toString"])来得到不同进制的数字字符串表示。例如,数字1的toString方法可以传入进制参数:(1)["toString"](2)得到二进制字符串"1",(1)["toString"](36)可以得到36进制下的"1",而36进制包含了数字0-9和字母a-z。
通过复杂的组合,JSFuck最终可以用这六个字符,表示出任意字符的Unicode码点,进而通过String.fromCharCode函数构造出任意字符串,最终拼接成完整的JavaScript代码。
注意:对于解题而言,我们不需要自己从头构造这些。但理解这个原理至关重要。当你看到一段JSFuck代码时,你能意识到它本质上是一段被“翻译”成特殊字符集的JavaScript,而不是不可读的加密数据。这种思维转换是解题的第一步。
2.3 JSFuck在CTF中的常见形态
在题目中,你通常会在以下几个地方遇到JSFuck:
- 网页源代码(
<script>标签内):这是最常见的形式,可能直接是一大段JSFuck代码,或者作为某个变量的值。 - 响应头或Cookie值:有时出题人会将JSFuck代码放在HTTP响应头(如
X-Data)或Cookie里,需要你查看网络请求才能发现。 - 经过其他编码(如Base64)二次处理:为了增加一点难度,出题人可能会先将JSFuck代码进行Base64编码,你需要先解码Base64,才能看到原始的JSFuck字符。
- 动态生成:通过AJAX请求获取,或由一段正常的JavaScript代码动态生成并执行。
LitCTF 2023的这道题,就属于第一种情况,非常直接。
3. 实战解密流程与工具链
理论清楚了,我们进入实战环节。拿到一段JSFuck代码,我一般的解密流程是:在线工具优先尝试 -> 浏览器控制台辅助验证 -> 必要时手动分析关键片段。
3.1 第一步:识别与提取
首先,打开题目给出的网页,按F12查看源代码。在<script>标签里,你可能会看到类似下面这样的内容(此为示例,非原题):
<script> [][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+(+[![]]+[])[+!+[]+[+!+[]+[+[]]]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(!+[]+!+[]+!+[]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(+![]+([]+[])[([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]])(+!+[]+[+[]]+[+!+[]])+(+![]+!+[]+!+[]+[!+[]+!+[]])+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]])()</script>我们的任务就是把这一长串字符复制出来。一个关键的细节:复制时务必检查开头和结尾,确保没有遗漏字符。有时JSFuck代码会被分在多行,或者夹杂在HTML注释中,需要仔细查看。
3.2 第二步:使用在线解密工具
对于99%的CTF题目,在线工具都能直接搞定。这里推荐几个我常用的,它们各有特点:
JSFuck Decoder (https://enkhee-osiris.github.io/Decoder-JSFuck/):
- 优点:界面简洁,解密速度快,支持大段代码。解密后可以直接在页面的模拟控制台里执行,看到结果。
- 用法:将JSFuck代码粘贴到左侧输入框,点击“Decode”按钮,右侧就会显示解密后的JavaScript代码。
- 实战技巧:解密后,不要只看代码,一定要关注工具下方可能出现的“执行结果”。有时解密出的代码会直接输出flag或关键信息。
CTF在线工具集(如题目中提到的那个):
- 这类网站通常集成了JSFuck、JJEncode、AAEncode等多种编码的解密功能。
- 优点:一站式解决多种编码问题,适合在比赛中快速切换。
- 注意:有些工具站广告较多,需要仔细找到正确的输入框和按钮。
浏览器扩展(如 HackTools):
- 对于经常打CTF的同学,可以在浏览器安装集成了编码解码工具的扩展。这样无需离开题目页面,就能快速解密,效率极高。
以LitCTF 2023的题目为例,将源代码中的JSFuck代码完整复制,粘贴到上述任一工具的输入框,点击解密。你通常会得到一段清晰得多的JavaScript代码。
3.3 第三步:分析与执行解密后的代码
工具解密出的代码,可能仍然不是最终答案。常见的情况有以下几种:
情况一:代码直接包含flag。解密后的代码可能像这样:
console.log("flag{th1s_1s_an_3x4mpl3_fl4g}");或者通过alert、document.write输出。这种情况下,flag已经直接呈现在你面前。
情况二:代码是逻辑判断或计算。解密后的代码可能是一个函数,用于验证用户输入,或者计算出一个值。例如:
function check(input) { if (input === 'some_secret_key') { return 'flag{...}'; } else { return 'Wrong!'; } }这时,你需要分析这个逻辑,找到正确的输入(some_secret_key),或者直接运行这个函数,看看它返回什么。一个非常重要的技巧:将解密后的代码复制到浏览器控制台(F12 -> Console)中执行。如果它是一个自执行函数,可能会直接输出结果;如果是一个定义好的函数,你可以手动调用它。
情况三:代码再次进行其他操作(如网络请求)。有时,JSFuck代码解密后,会执行一个fetch或XMLHttpRequest,去另一个地址获取真正的flag。这时你需要按照解密代码的指示,去访问那个URL,或者查看网络请求记录。
在LitCTF 2023的这道题中,解密后的代码很可能是一个简单的字符串操作或变量赋值,直接指向了flag。
3.4 第四步:当在线工具失效时的备选方案
虽然很少见,但偶尔你会遇到一些“非标准”或经过修改的JSFuck变种,导致在线工具无法正确解密。这时不要慌,可以尝试以下方法:
使用Node.js环境手动执行:
- 将JSFuck代码保存为一个
.js文件,比如challenge.js。 - 在命令行中运行
node challenge.js。 - 原理:JSFuck本身就是有效的JavaScript,Node.js引擎可以直接解析并执行它。如果代码设计为输出内容,你会在终端看到结果。
- 风险提示:绝对不要随意执行来源不明的JS代码,尤其是在本地有重要数据的机器上。CTF题目通常是在隔离的虚拟环境或赛方提供的平台上进行,风险可控。在不确定的情况下,最好使用在线的代码沙盒环境。
- 将JSFuck代码保存为一个
浏览器控制台直接执行:
- 这是最直接的方法。在题目页面的控制台里,直接粘贴整段JSFuck代码并回车。
- 如果代码是立即执行的表达式,你会立刻看到结果。
- 如果代码只是定义了函数或变量,你需要根据上下文去调用或查看它们。
- 优点:环境与题目完全一致,避免了因环境差异导致的问题。
分段分析与解密:
- 如果代码非常长,可以尝试将其分成几段,分别用在线工具解密,然后拼凑起来理解逻辑。
- 或者,寻找代码中可能输出信息的部分,比如
(alert)、(console.log)等构造,重点解密这些部分。
4. 进阶技巧与深度排查指南
掌握了基本流程,你已经能解决大部分JSFuck题目。但要成为高手,还需要一些进阶技巧来应对更复杂的情况。
4.1 识别混合编码与多层嵌套
出题人不会总是直接给你一段纯净的JSFuck。常见的“套娃”手法有:
- JSFuck + Base64:你看到的可能是一段Base64字符串。解码后,得到的才是JSFuck代码。在CTF中,遇到一段看似随机但字符集符合Base64的字符串,先尝试Base64解码是良好习惯。
- JSFuck + 其他JS混淆(如JJEncode, AAEncode):JJEncode使用
$、_等字符,AAEncode使用颜文字。有时会先进行其他混淆,再转成JSFuck。这就需要你根据字符特征(比如大量()、[]是JSFuck,大量$是JJEncode)来判断第一层是什么,逐层剥离。 - JSFuck作为字符串被eval:解密后得到的代码可能是
eval("另一段JSFuck代码字符串")。这时你需要对里面的字符串再次进行JSFuck解密。
排查流程:
- 观察字符集。如果全是
[ ] ( ) + !,直奔JSFuck解密。 - 如果结尾有
=或字符符合A-Za-z0-9+/,尝试Base64解码。 - 如果解密后得到的结果仍然是不可读的代码,观察其结构,判断是否属于其他类型的混淆。
4.2 利用浏览器开发者工具进行动态调试
对于复杂的、交互式的题目,静态解密可能不够。你需要动态跟踪代码执行。
Sources面板设置断点:
- 在解密后的JS代码关键行(如
if判断、return语句)左侧点击设置断点。 - 刷新页面或触发相应操作,代码执行会在断点处暂停。
- 此时你可以查看当前所有变量的值,单步执行(F10),步入函数(F11),这对理解代码逻辑有巨大帮助。
- 在解密后的JS代码关键行(如
Console面板交互:
- 在代码暂停时,你可以在Console里直接输入变量名查看其值,甚至修改变量值来改变执行流程(这在某些“绕过”类题目中很有用)。
Network面板监控请求:
- 如果JS代码发送了网络请求(获取flag),Network面板会记录下所有的请求和响应。这里往往是flag的最终藏身之处。
4.3 编写简易解密脚本应对特殊变种
如果题目对JSFuck做了微小改动(比如替换了某个字符),导致通用工具失效,你可以根据其原理,自己写一个简单的替换脚本。例如,如果题目把!换成了~,你可以先用文本编辑器的替换功能,把~全部换回!,然后再用工具解密。
更一般的情况,你可以写一个Node.js脚本,利用eval直接执行JSFuck代码并捕获输出:
// decrypt.js const fs = require('fs'); let jsfuckCode = fs.readFileSync('challenge.txt', 'utf-8'); try { // 注意:在生产环境或不确定代码安全性时,严禁这样做! let result = eval(jsfuckCode); console.log('执行结果:', result); } catch (error) { console.error('执行出错:', error.message); // 有时代码不直接输出,而是定义了全局变量 // 可以尝试检查全局对象 console.log('尝试查看全局变量...'); }重要警告:
eval会执行任意代码,极度危险!仅限在完全可控、隔离的CTF比赛环境中使用,切勿用于处理未知来源的代码。
5. 常见问题与避坑实录
在这一部分,我总结了一些新手在解JSFuck题目时最容易遇到的问题,以及我的解决方法。
5.1 工具解密后是乱码或空白
- 可能原因1:复制不完整。JSFuck代码通常很长,手动复制时容易漏掉开头或结尾的几个字符。务必使用鼠标全选,或者查看网页源代码(Ctrl+U)直接复制整个
<script>标签内的内容。 - 可能原因2:存在不可见字符。网页中可能夹杂着换行符、制表符或零宽空格,影响解密。尝试将代码粘贴到纯文本编辑器(如VS Code、Notepad++)中,显示所有字符,清理掉非
[ ] ( ) + !之外的任何字符。 - 可能原因3:代码被HTML实体编码。例如
<代表<。在网页源代码里,JSFuck代码可能被这样编码以正常显示。你需要先对这段代码进行HTML解码,得到原始的<、>等符号(虽然JSFuck本身不用这些符号,但包裹它的脚本标签可能需要)。在线HTML解码工具很容易找到。 - 解决方案:先确保代码纯净,再尝试不同的在线工具。如果都不行,尝试在浏览器控制台直接执行,看是否有错误信息。
5.2 解密出的代码看不懂或无法执行
- 可能原因:解密出的代码可能被“压缩”或“混淆”过(例如变量名都是a,b,c),或者它只是一个复杂的表达式,需要进一步计算。
- 解决方案:
- 格式化代码:使用在线的JavaScript代码美化工具(如 Prettier.io),让代码结构清晰。
- 重命名变量:如果变量名无意义,尝试根据其用途赋予有意义的名称,便于理解。
- 分段执行:在浏览器控制台里,将长表达式拆分成几部分,分别执行并查看中间结果。例如,如果解密结果是
(a+b)(c),你可以先执行console.log(a),console.log(b),看看a和b到底是什么。 - 搜索关键函数:在解密后的代码中搜索
alert、prompt、console.log、document.write、fetch、location等关键字,它们通常是输出或跳转的关键。
5.3 执行后没有反应或报错
- 可能原因1:代码依赖浏览器特定环境。有些JSFuck代码可能调用了
document、window等对象,在Node.js环境下执行就会报错。务必在浏览器控制台执行。 - 可能原因2:代码需要特定触发条件。解密出的代码可能是一个函数,需要特定的事件(如点击按钮、输入内容)才会被调用。你需要查看网页的HTML结构,找到绑定事件的元素,或者手动在控制台调用这个函数。
- 可能原因3:代码存在语法错误。极少数情况下,出题人可能故意设置了一个小陷阱。仔细检查解密后的代码,或者用浏览器的“Sources”面板看看是否有语法错误提示。
- 解决方案:始终优先在题目本身的浏览器环境中执行代码。如果是一个函数,尝试找出它的名字并直接调用,例如
window.onload()或checkFlag(“test”)。
5.4 如何提高解题速度
- 建立工具书签:将几个可靠的JSFuck解密、Base64解码、代码美化网站加入浏览器书签栏,一键直达。
- 训练识别能力:看到
[][(![]+[])...这种模式,立刻反应是JSFuck,而不是去思考它的逻辑。 - 养成检查习惯:拿到Web题,F12看源代码是标准动作。不仅看
<script>,还要看<body>内的内联脚本、注释,以及Network中的JS文件。 - 利用浏览器控制台作为主战场:解密、执行、调试、查看变量,全部在控制台完成,效率最高。
回过头看LitCTF 2023这道题,它更像是一个标准的“热身题”,目的是让选手熟悉JSFuck这种形式。通过这道题,我们巩固了从识别、提取到使用工具解密、分析执行结果的全过程。在更复杂的比赛中,JSFuck可能只是整个链条中的一环,后面或许接着逆向分析、密码学解密或SQL注入。但无论如何,扎实地掌握这种基础编码的解密方法,是你在CTF赛场上稳步前进的可靠基石。下次再看到满屏的括号和叹号,你大可以自信地打开收藏夹里的工具,开始你的“解码”表演了。