请大家先观察下面这条JavaScript语句:
̀̀̀̀̀́́́́́̂̂̂̂̂̃̃̃̃̃̄̄̄̄̄̅̅̅̅̅̆̆̆̆̆̇̇̇̇̇̈̈̈̈̈̉̉̉̉̉̊̊̊̊̊ͅͅͅͅͅͅͅͅͅͅͅalert(̋̋̋̋̋̌̌̌̌̌̍̍̍̍̍̎̎̎̎̎̏̏̏̏̏ͅͅͅͅͅ1̐̐̐̐̐̑̑̑̑̑̒̒̒̒̒̓̓̓̓̓̔̔̔̔̔ͅͅͅͅͅ)̡̡̡̡̡̢̢̢̢̢̛̛̛̛̛̖̖̖̖̖̗̗̗̗̗̘̘̘̘̘̙̙̙̙̙̜̜̜̜̜̝̝̝̝̝̞̞̞̞̞̟̟̟̟̟̠̠̠̠̠̣̕̕̕̕̕̚̚̚̚̚ͅͅͅͅͅͅͅͅͅͅͅͅͅͅͅ
写在前面的话
当Twitter宣布将推文字符限制从140提升到280之后,我就想弄清楚在这种新的字符限制下,如何使用Unicode字符来“搞事情”。我测试了某些特殊字符,并在Twitter上引起了一些显示错误,即所谓的ZalgoScript。此时我就想知道如何自动化地去识别这些字符,因为你无法通过查看页面DOM树结构来发现某些字符的特殊行为,我们需要截图才能知道浏览器所要呈现的视图。我一开始使用的是JavaScript和canvas技术来进行截图,但生成的图片并不匹配浏览器中真正呈现的画面。所以我需要其他的方法,Headless Chrome就是最佳的选择。我在这里使用了puppeteer,这是一个NodeJS模块,允许我们控制Headless Chrome并获取截图。
生成字符
为了生成Zalgo字符,你可以重复使用单个字符,或者使用两个字符并重复第二个字符。下面的码点可以通过自我重复来产生视觉效果,它们大多都是Unicode合并字符:
834,1425,1427,1430,1434,1435,1442,1443,1444,1445,1446,1447,1450,1453,1557,1623,1626,3633,3636,3637,3638,3639,3640,3641,3642,3655,3656,3657,3658,3659,3660,3661,3662
比如说,下面的JavaScript代码可以利用上面的某些字符来生成非常难看的文本:
<script>document.write(String.fromCharCode(834).repeat(20))</script>
得到的结果为: ͂͂͂͂͂͂͂͂͂͂͂͂͂͂͂͂͂͂͂͂
有趣的是,如果合并使用多个字符的话,还可以产生不同的效果。比如说,用字符311和844合并之后,同样的技术会产生如下的效果:
<script>document.write(String.fromCharCode(311)+String.fromCharCode(844).repeat(20))</script>
得到的结果为:ķ͌͌͌͌͌͌͌͌͌͌͌͌͌͌͌͌͌͌͌͌
构建模糊测试器
模糊测试器的构建其实非常简单,首先我们需要一个用来呈现字符效果的Web页面,然后通过CSS来增加页面宽度,并让正常字符平移到页面右侧,这样我们就可以检测页面的左侧、顶部和底部了,我把需要测试的div元素放在了页面中心。
下图为字符“a”和“b”呈现在fuzzer中的效果,为了让大家更好地理解fuzzer的处理过程,我们把fuzzer检查的地方在图中进行了标注:
而上述代码所呈现的字符ķ和~的码点为311和834,fuzzer也记录下了这两个字符所生成的有趣的视图效果:
<style>
.parent{
position: absolute;
height: 50%;
width: 50%;
top: 50%;
-webkit-transform: translateY(-50%);
-moz-transform: translateY(-50%);
-ms-transform: translateY(-50%);
-o-transform: translateY(-50%);
transform: translateY(-50%);
}
.fuzz{
height: 300px;
width:5000px;
position: relative;
left:50%;
top: 50%;
transform: translateY(-50%);
}
</style>
</head>
<body>
<divclass="parent">
<div class="fuzz"id="test"></div>
</div>
<script>
varchars = location.search.slice(1).split(',');
if(chars.length> 1) {
document.getElementById('test').innerHTML =String.fromCharCode(chars[0])+String.fromCharCode(chars[1]).repeat(100);
}else {
document.getElementById('test').innerHTML =String.fromCharCode(chars[0]).repeat(100);
}
</script>
JavaScript会从查询字符串中读取一到两个字符编码,然后使用innerHTML和String.fromCharCode来输出它们,这一切都是在客户端实现的。
然后在NodeJS中,我还需要使用PNG库和puppeteer库:
const PNGReader = require('png.js');
const puppeteer = require('puppeteer');
接下来,我可以使用下面这两个函数来判断某个像素是否为白色,并且是否位于目标区域中(顶部、左侧、右侧或底部):
function isWhite(pixel) {
if(pixel[0] === 255 && pixel[1] ===255 && pixel[2] === 255) {
return true;
} else {
return false;
}
}
function isInRange(x,y) {
if(y <= 120) {
return true;
}
if(y >= 220) {
return true;
}
if(x <= 180) {
return true;
}
return false;
}
fuzzBrowser()是一个异步函数,可以进行屏幕截图,也可以利用png库来读取png文件。这个函数可以将我们的特殊字符输出到控制台以及chars.txt文件之中。
async function fuzzBrowser(writeStream, page, chr1, chr2) {
if(typeof chr2 !== 'undefined') {
awaitpage.goto('http://localhost/visualfuzzer/index.php?'+chr1+','+chr2);
} else {
awaitpage.goto('http://localhost/visualfuzzer/index.php?'+chr1);
}
await page.screenshot({clip:{x:0,y:0,width:400,height: 300}}).then((buf)=>{
var reader = new PNGReader(buf);
reader.parse(function(err, png){
if(err) throw err;
outerLoop:for(let x=0;x<400;x++) {
for(let y=0;y<300;y++) {
if(!isWhite(png.getPixel(x,y))&& isInRange(x,y)) {
if(typeof chr2 !== 'undefined') {
writeStream.write(chr1+','+chr2+'n');
console.log('Interesting chars:'+chr1+','+chr2);
} else {
writeStream.write(chr1+'n');
console.log('Interesting char:'+chr1);
}
break outerLoop;
}
}
}
});
});
}
接下来,我需要构造一个匿名异步函数,用来循环处理目标字符并调用fuzzBrowser()函数。我对多种字符组合场景进行了测试,并排除掉了一些可能会出现错误的字符:
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const singleChars
={834:1,1425:1,1427:1,1430:1,1434:1,1435:1,1442:1,1443:1,1444:1,1445:1,1446:1,1447:1,1450:1,1453:1,1557:1,1623:1,1626:1,3633:1,3636:1,3637:1,3638:1,3639:1,3640:1,3641:1,3642:1,3655:1,3656:1,3657:1,3658:1,3659:1,3660:1,3661:1,3662:1};
const fs = require('fs');
let writeStream =fs.createWriteStream('logs.txt', {flags: 'a'});
for(let i=768;i<=879;i++) {
for(let j=768;j<=879;j++) {
if(singleChars[i] || singleChars[j]) {
continue;
}
process.stdout.write("Fuzzingchars "+i+","+j+" r");
await fuzzBrowser(writeStream, page, i,j).catch(err=>{
console.log("Failed fuzzingbrowser:"+err);
});
}
}
await browser.close();
await writeStream.end();
})();
ZalgoScript
就在不久之前,我在Edge浏览器中发现了一个非常有趣的漏洞,简单来说,Edge会错误地将某些特殊字符当成空白字符,而某些Unicode字符组合在一起的时候就会触发这种漏洞。因此,我们只需要将Zalgo应用到这里,就能够利用该漏洞,并得到我们想要的ZalgoScript了。首先,我们需要生成一份字符列表,而Edge会将该列表中所有的字符都当作空白字符。这里我选择的是768-879之间的字符,根据测试结果显示,字符837跟768-879之间的字符如果组合在一起的话,将产生非常好的视觉效果。因此,我循环遍历这个列表来进行字符组合,最终生成的代码既是ZalgoScript,又是JavaScript。
a=[];
for(i=768;i<=858;i++){
a.push(String.fromCharCode(837)+String.fromCharCode(i).repeat(5));
}
a[10]+='alert('
a[15]+='1';
a[20]+=')';
input.value=a.join('')
eval(a.join(''));
这也就是我们文章开头̀̀̀̀̀́́́́́̂̂̂̂̂̃̃̃̃̃̄̄̄̄̄̅̅̅̅̅̆̆̆̆̆̇̇̇̇̇̈̈̈̈̈̉̉̉̉̉̊̊̊̊̊ͅͅͅͅͅͅͅͅͅͅͅalert(̋̋̋̋̋̌̌̌̌̌̍̍̍̍̍̎̎̎̎̎̏̏̏̏̏ͅͅͅͅͅ1̐̐̐̐̐̑̑̑̑̑̒̒̒̒̒̓̓̓̓̓̔̔̔̔̔ͅͅͅͅͅ)̡̡̡̡̡̢̢̢̢̢̛̛̛̛̛̖̖̖̖̖̗̗̗̗̗̘̘̘̘̘̙̙̙̙̙̜̜̜̜̜̝̝̝̝̝̞̞̞̞̞̟̟̟̟̟̠̠̠̠̠̣̕̕̕̕̕̚̚̚̚̚ͅͅͅͅͅͅͅͅͅͅͅͅͅͅͅ的由来。
资源获取
可视化Fuzzer:【源代码】
Zalgo文本生成器:【传送门】
* 参考来源:portswigger,FB小编Alpha_h4ck编译,转载请注明来自FreeBuf.COM