利用视觉模糊测试技术探索Z͌̈́̾a͊̈́l͊̿g̏̉͆o̾̚̚S̝̬ͅc̬r̯̼͇ͅi̼͖̜̭͔p̲̘̘̹͖t̠͖̟̹͓͇ͅ

请大家先观察下面这条JavaScript语句:

̀̀̀̀̀́́́́́̂̂̂̂̂̃̃̃̃̃̄̄̄̄̄̅̅̅̅̅̆̆̆̆̆̇̇̇̇̇̈̈̈̈̈̉̉̉̉̉̊̊̊̊̊ͅͅͅͅͅͅͅͅͅͅͅalert(̋̋̋̋̋̌̌̌̌̌̍̍̍̍̍̎̎̎̎̎̏̏̏̏̏ͅͅͅͅͅ1̐̐̐̐̐̑̑̑̑̑̒̒̒̒̒̓̓̓̓̓̔̔̔̔̔ͅͅͅͅͅ)̡̡̡̡̡̢̢̢̢̢̛̛̛̛̛̖̖̖̖̖̗̗̗̗̗̘̘̘̘̘̙̙̙̙̙̜̜̜̜̜̝̝̝̝̝̞̞̞̞̞̟̟̟̟̟̠̠̠̠̠̣̕̕̕̕̕̚̚̚̚̚ͅͅͅͅͅͅͅͅͅͅͅͅͅͅͅ

利用视觉模糊测试技术探索Z͌̈́̾a͊̈́l͊̿g̏̉͆o̾̚̚S̝̬ͅc̬r̯̼͇ͅi̼͖̜̭͔p̲̘̘̹͖t̠͖̟̹͓͇ͅ

写在前面的话

当Twitter宣布将推文字符限制从140提升到280之后,我就想弄清楚在这种新的字符限制下,如何使用Unicode字符来“搞事情”。我测试了某些特殊字符,并在Twitter上引起了一些显示错误,即所谓的ZalgoScript。此时我就想知道如何自动化地去识别这些字符,因为你无法通过查看页面DOM树结构来发现某些字符的特殊行为,我们需要截图才能知道浏览器所要呈现的视图。我一开始使用的是JavaScript和canvas技术来进行截图,但生成的图片并不匹配浏览器中真正呈现的画面。所以我需要其他的方法,无头浏览器Chrome就是最佳的选择。我在这里使用了puppeteer,这是一个NodeJS模块,允许我们控制无头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检查的地方在图中进行了标注:

利用视觉模糊测试技术探索Z͌̈́̾a͊̈́l͊̿g̏̉͆o̾̚̚S̝̬ͅc̬r̯̼͇ͅi̼͖̜̭͔p̲̘̘̹͖t̠͖̟̹͓͇ͅ

而上述代码所呈现的字符ķ和~的码点为311和834,fuzzer也记录下了这两个字符所生成的有趣的视图效果:

利用视觉模糊测试技术探索Z͌̈́̾a͊̈́l͊̿g̏̉͆o̾̚̚S̝̬ͅc̬r̯̼͇ͅi̼͖̜̭͔p̲̘̘̹͖t̠͖̟̹͓͇ͅ

<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