这两道题真是太有趣了!虽然标签是逆向,但是以前端为载体,有很多JS/CSS奇淫巧计,我已经迫不及待地想要和大家分享了。
Treasure Map
题目地址:http://treasure.chal.pwni.ng/
Ready your masts and set sail! Thar be treasure here if we can figure out how to find it.
Buried Treasure
Follow the map and get the booty — a pirate’s work is never done.
题目
这道题包含了success.js、fail.js和 0.js ~ 199.js 共两百个一模一样的 js 文件(引用的SourceMap有所不同)。
0.js
fail.js
success.js
大概的校验流程是这样的:
- 输入一个有效格式的Flag
- 存入 window.buffer
- 调用
go()
方法 (这个方法在上述202个脚本中均存在,网页默认引用了 0.js,所以执行 0.js 里的go()
方法) - 通过某种算法找到这202个脚本中的另一个进行载入,执行其中的
go()
方法
分析
最终的目的是让脚本能够载入success.js
并执行。但是所有脚本的内容都是一样的,可能需要从SourceMap下手。
上面是 0.js 的 SourceMap,对当前源码进行了详细的映射,那么具体映射了什么呢?
SourceMap中的
mappings
包含VLQ编码,分号用于表示文件行,逗号表示位置,VLQ编码的部分是一个可变长数组,代表了映射所需的各个增量,具体可以参考文章http://ruanyifeng.com/blog/2013/01/javascript_source_map.html
根据SourceMap的映射规则,脚本的2-66行(即b64变量的内容)被分别映射到不同的66个文件中,举个简单的例子,0.js的映射关系大概是这样的:
而0.js-199.js中的代码部分,实际上就是在对SourceMap进行解析,从传入的flag依次取出字符,对应到特定的js文件。
例如对于一个B开头的flag,就会去请求118.js,解析118.js的SourceMap,并处理flag的第二个字符,以此类推。
我们可以尝试去寻找哪个文件包含对 success.js 的映射,这样就可以确定 Flag 的最后一个字符和其对应文件,一步一步反推就能得到最终的Flag。
相关脚本
下载文件
首先当然是要把所有 SourceMap 给下载下来,这里提供一个 NodeJS 脚本
脚本会将所有的 SourceMap 下载到 originmaps 文件夹中。(记得提前创建文件夹)
解析 SourceMap
稍微修改一下题目给的 js,解析SourceMap,并将映射表保存到文件中。
该文件使用了utils.js,可以在这里下载:https://ipfs.4everland.xyz/ipfs/QmSDubw4sHg25kSjzzQu9aotV52bbbRp9nZbxog5KcbfDX
寻找正确的加载路径
错了🤯🤯
最终我们能得到一个23位的Flag:Nd+a+map/How+about+200!
,但是题目要求25位。说明中间可能会存在多条路径,下面的脚本是柏喵改进的,tql
之前错误 Flag 的路径是0.js
->137.js
->160.js
->192.js
->… (Nd+a…)
正确 Flag 的路径是0.js
->137.js
->23.js
->137.js
->160.js
->192.js
->… (Need+a…)
得到正确的 FlagPCTF{Need+a+map/How+about+200!}
CSS
题目地址:https://plaidctf.com/files/css.74486b61b22e49b3d8c5afebee1269e37b50071afbf1608b8b4563bf8d09ef92.html
I found this locked chest at the bottom o’ the ocean, but the lock seems downright… criminal. Think you can open it? We recommend chrome at 100% zoom. Other browsers may be broken.
这道题是一个完全由CSS构成的密码锁。前天我就发了推吐槽这道题的CSS样式。(能整出这种活的人真是太牛了)
分析
Flag 包含小写字母a
–z
以及下划线_
,以三个字符为一组,分成了 14 组共 42 位。
字符的选择是通过 details 标签来实现的,details 的子元素拥有不同的高度,使用 css 的calc
函数来获取高度并运算,得到字符元素的偏移量,达到显示字符的目的。
每3个字符会控制4个SVG蒙版。拿了8个来举例子:
灰色部分是每个SVG透明的位置,倘若每个SVG的位置正确,最终应该是这个效果:
由于details的伸缩与展开会影响到父容器高度,SVG蒙版的父元素也在这个容器中,高度也会发生改变,而SVG的top
属性通过父元素高度计算得来。
庆幸的是,SVG以dataurl的方式作为背景图片,但是这个元素的高度是固定的,所以不需要考虑背景的各种填充方式(cover
/contain
等),换句话说,想要解出正确的Flag,SVG 最终的top
值一定只和其透明位置的高度有关。
解题
我没有去看各个 detail 标签和 SVG 容器的高度是如何变化的,这实在太多了(或许可以尝试使用 Typed OM 辅助分析?)
不过每组SVG只由3个字符控制,也就是最终只需要尝试27^3种情况,决定直接通过暴力遍历的方式来解决。
这里就有一个问题:如何知道SVG已经到了正确的位置?
同学想用无头浏览器进行图像匹配,这显然是行不通的,效率太低,且需要对每组SVG进行隔离才能正确识别。
我想到的是,如果能够获取到每个SVG的top
值,那么就可以通过计算得到其透明位置的高度,然后与预期的高度进行比较,如果相等,那么就说明这个SVG已经到了正确的位置。
这其实很好办,分成下面几步:
- 如何拿到当前SVG的
top
值?
SVG 的top
样式是通过calc
计算得来的,可能一开始会觉得很难获取,但实际上,浏览器提供了接口window.getComputedStyle
,通过这个接口,能够得到元素计算之后的样式数值。
- 每个SVG中透明区域的位置在哪?
上面这个SVG的透明区域是M2 22V38H198V22Z
,不难看出,这个区域的上边界是22
,不过由左边界2
可以看出,开发者可能希望透明区域具有2px
的边距,大胆猜测,透明区域的上边界应该是20
。
所有SVG蒙版都被处理成 dataurl ,以内联背景样式的形式嵌入到页面中,这也为编写脚本提供了极大的便利。只需要按次序取出蒙版,然后使用正则匹配出透明区域的上边界即可。
得到的结果是:
- 每个SVG最终正确的
top
值应该是多少?
需要注意的是:原HTML中绿色currect!
字样在距离顶部60px
处,这个值也需要考虑进去。
可以得到所有SVG最终所需的正确top
值:
我们尝试将每个 SVG 的top
值设置为正确的值,显示出了绿色的currect!
字样,说明这些位置是正确的!
后来想想其实getBoundingClientRect().y
也能拿到,绕了个大弯
- 如何改变某一位字符?
这是最关键的,因为我们需要通过改变某一位字符来改变SVG的top
值,从而达到移动SVG的目的。
然而这个网页完全由CSS实现,想直接修改字符当然是行不通的。
那么模拟点击两个红色上下箭头能行么?也不行。网页在每个箭头处堆叠了26个detail标签,通过给detail设置偏移来实现红色箭头位置在每次点击时都能操作到不同的detail。
所以,只能尝试直接用js去操作detail标签属性。当detail打开时,其元素本身会具有open
属性。我们只需要操纵这个属性,就能实现打开和关闭detail标签的效果。
寻找规律能发现,倒数第一个 detail 打开时,字符为 a;倒数后二个 detail 打开时,字符为 b,以此类推。
下面是一个简单的脚本,用于改变某一位字符:
setCharOfSlot(0,'b')
即将第0
位设置为b
接下来,万事俱备,可以开始尝试遍历了!使用 requestAnimationFrame 来控制遍历速度避免卡顿,同时支持了进度保存,跑出结果大概需要 3 分钟。