# Hacking with Unicode

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2014-06-09T06:58:00Z
Canonical: https://wepostx.com/topics/492

> 乌云历史资料归档
>
> **原始作者：** mramydnei
> **原始编号：** superkieran-wooyundrops:273
> **原始发布时间：** 2014-06-09 14:58
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

from：https://speakerdeck.com/mathiasbynens/hacking-with-unicode

## 0x00 Unicode简介

很多人经常会把Unicode和utf-8的概念混淆在一起，甚至会拿它们去做比较。实际上这种比较是非常的荒谬的。这就犹如拿“苹果”和“僵尸”去做比较，当然我觉得可以是更夸张的例子。所以，首先需要声明的是Unicode不是字符编码。我们可以把Unicode看做是一个数据库，不同的码点(code point)会指向不同的符号（如下图所示）。这是一种很简单想法，当你说出一个符号的码点时，别人就会知道你在说的是什么符号。当然了，如果他不知道也可以去查询Unicode表。

![原文图片](/media/2016/06/2d5caab167e9a6e9c9cfe974d1830ace)

下面以拉丁字母中大写的A为例：

![原文图片](/media/2016/06/b242119cc272fa20997570cc75697aa2)

拉丁字母大写A的码点就是: U+0041。又比如码点U+2603就会是指向一个雪人(snowman)的符号：

![原文图片](/media/2016/06/850aa33ce9d5d881b43b276f75f95d78)

当然还有一些更有趣的符号，比如下面这位：

![原文图片](/media/2016/06/76036df61deaf6fa48b85e8671e513c0)

这个符号看上去是一坨屎在向你微笑，这符号虽然很搞笑，但这并不是重点。因为我们发现这次的c码点从4位变成了5位。那么码点的长度究竟应该是多少，又到底存在多少个码点呢？

```text
U+000000 -> U+10FFFF
```

上面这个范围就是码点的范围了，长度一目了然。至于真实存有符号的码点的数量就不是所有的码点的总和了。因为其中也包括一些预留位。 除此之外还需要我们了解的就是平面（plane）的概念。Unicode字符被分成17组编排，而其中的每一组我们都称其为平面（Plane）。 其中的第一组被称为基本多语言面（Basic Multilingual Plane)也可以简称为BMP。是我们平时最常用的平面。它的范围是: U+0000 -> U+FFFF 举个例子，如果你正在使用英语来来编写一篇文章，那么你所用的字符就可能全都来自这一个平面。其余的平面2-17的范围则是:

```text
U+010000 -> U+10FFFF
```

这16个平面占据了整个Unicode的75%，我相信来自这些平面的字符很容易就可以和基本多语平面做区分。因为只要码点的长度大于4，你就应该想到这个字符是来自2-17的平面。

## 0x01 编码

在我们对Unicode的基础知识有了一点了解之后，现在再让我们来谈谈编码。

![原文图片](/media/2016/06/be4ddc2746305ca9f3074306d7871ab9)

上图中所展示的UTF-8,UTF-16,UTF-32,UTF-EBCDIC和GB18030都是一些完全支持Unicode的编码。我们发现这种编码数量并不多，而且它们也存在一些差异。拿UTF-32来讲，我们发现不论需要编码的字符是什么，这个编码都会使用4 bytes。从存储空间的角度来讲，这种设计是十分的不合理的。相对而言UTF-16会显得更合理一些，不过不难看出其中最合理的还是属UTF-8。比如，我们用utf-8编码的一个字符，如果是属于第一个区域(U+000000-U+00007F)那么它就只会占用1 bytes。 在探索编码的过程当中，你最多可能会被搜索引擎带到的可能是下面的页面：

![原文图片](/media/2016/06/83ce832240421dff018ad650ac5d13d4)

如同你所看到的，这是IETF发布的RFC文档。很多人在去翻阅文档时，都会选择看RFC文档。原因很简单因为RFC是标准。在RFC文档中我们甚至可以查阅到Unicode是如何被设计的。但是作为黑客，我想这些应该不是你所感兴趣的。在这里我会更加推荐你去看下面的网页：

![原文图片](/media/2016/06/51b0a0d10eda417fca84cce37d5978c3)

在Encoding Living Standard你可以阅读到一些更贴近真实世界的东西。不是因为对RFC有偏见，而是因为RFC中的标准和实际应用到浏览器中的情况往往都会存在一些差异。

## 0x02 JavaScript中的Unicode

在这个部分中，我们主要会谈论JavaScript中一些奇怪的现象，一些程序员会犯的错误以及如何去修复。就像很多其它语言一样，JavaScript中也会有很多奇怪的现象。比如有这样的网站来专门纰漏PHP中一些奇怪的现象。

![原文图片](/media/2016/06/ca8462ddccc48158626c793afb81c80c)

同样的我们也有wtfjs。上面会纰漏一些JavaScript中的奇怪现象:

![原文图片](/media/2016/06/a93fe0161a5cffcfe6a7366a66a60c5f)

当然还有一些其它的问题。比如，比较著名的Punycode攻击，常用于注册看上去相似的域名进行钓鱼攻击(如下图所示)。对于Punycode攻击也有一些非常有意思的文档。比如，Mutillidae的作者Adrian在不久前发表的” Fun and games with Unicode”。

![原文图片](/media/2016/06/a14ae2d95f59e3b4e4c439da304e655e)

我们都知道在Javascript当中我们可以使用base16来表示一个字符，比如： 用 ‘x41’ 来表示大写拉丁字母”A” 我们也可以用Unicode来表示一个字符，比如： 用’u0041’也可以表示大写拉丁字母”A” 那我们之前提到的U+1F4A9（那个在向你微笑的一坨屎）又是怎么样的呢？这个问题说简单也简单，说复杂也复杂。在ECMAScript 6（下面将简称为ES）中我们可以这样来解决这个问题：

![原文图片](/media/2016/06/69e6361736c5c54d711591460d3175db)

当然，我们也可以使用代理编码对（surrogate pairs）:

![原文图片](/media/2016/06/070ded73bb778166436aa037ca49d0e2)

下面再附上代理编码对的编码。当然了，你不需要真的去记住它。但如果你是一个经常会和Unicode打交道的人，那么你至少应该去去了解一下。

![原文图片](/media/2016/06/db63f5012b965965225e54a7c294f911)

现在我们再聊一下字符串长度的问题。这也是一些开发者会常犯的错误。在这里需要我们记住的是，字符串长度并不等于字符个数。下面这张图应该很容基就能帮助我们理解到这个问题。

![原文图片](/media/2016/06/0c1a8030d010818e1f9c1bcfd6d82f3c)

不要认为没有人会犯这样的错误。Twitter上就有这样的问题，我实际上应该被允许发140坨屎,但实际上我只能发70坨……。虽然这不是安全问题，但这仍然是个逻辑错误。

![原文图片](/media/2016/06/4c635784e92b90c8c016e47f5fe159a0)

对于处理这个问题你可以使用我编写的punycode.js:

![原文图片](/media/2016/06/977110a019ccc96a9585eb76bdc8cc8f)

当然在ES6中也有别的解决方案：

![原文图片](/media/2016/06/9b5307b0d4c96d38d44f0d2b483b0455)

但实际上问题往往没有上面描述的那么简单。因为javascript还对会一些字符进行escape：

![原文图片](/media/2016/06/ea22187945ed9d97aa308ae370b903cd)

这个问题甚至会比我们想象的还要更复杂一些：

![原文图片](/media/2016/06/8dd4db2d9d4d110b08f9b1125e831e3f)

所以ES6又为我们提供了Unicode标准化：

![原文图片](/media/2016/06/1639c7ecb25291733a1303818c52b634)

那么这个方案已经完美了么？其实也不然。因为我们往往会面临更复杂的挑战。比如下面的例子，我们看到的只有9个字符，然而结果会是116。修复这类问题我们还需要用到epic regex-fu.

![原文图片](/media/2016/06/9c8a2dee21d7fa1d9fa9239eda9dc6f2)

同样的问题也存在于reverse函数中（看图中第三个reverse结果）

![原文图片](/media/2016/06/8bd880b1f409a39f4964f2bab01badd4)

我们可以通过下图中提供的esrever来解决这个问题：

![原文图片](/media/2016/06/c6a2e4a2fe283e208f04d43d79c7a913)

又比如String.fromCharCode()函数也只能处理U+0000~U+FFFF范围内的字符。在解决这个问题上时，我们依旧可以使用之前提到的代理编码对，Punycode.js又或者是ES6

![原文图片](/media/2016/06/c7e5f037316086d290f281d0a324153c)

除此之外还有charAt()和charCodeAt()函数只能取一半的unicode的问题（U+1F4A9只能取到U+D83D)也在ES6和ES7当中得到了解决。在ES7可以用at()来替代charAt（据说这个问题没赶上ES6的修正）,在ES6中的可以使用codePointAt来代替charCodeAt()。存在类似问题的函数还有很多，比如：slice(),substring()等等等等。 然后就是一些正则匹配的问题。也分别在ES6当中得到了解决。（翻到这儿感觉像是推ES6的有没有……）

![原文图片](/media/2016/06/f79c45ec9f6253f196e54e6f2d62ecd0)

当然这些也许只是JS开发者在开发的过程当中会碰到的问题的一小部分。对于测试这类问题我推荐你使用“那坨屎”来进行验证^_^

## 0x03 Mysql中的Unicode

谈完了JavaScript中Unicode问题之后，让我们再来看看Mysql中Unicode的表现又是如何的。下面是个很常见的例子，网上也有很多Mysql教程是教你这么去建数据库的。

![原文图片](/media/2016/06/c2397667d24f7387b0ad0eaf97d1ef10)

请注意这里的编码选择的是UTF-8。这也会有问题？请看下图：

![原文图片](/media/2016/06/af060494e30f8fdd74eb4241e28ce619)

从上图中我们可以看到当我们使用UTF－8编码来建立数据库，并试图更新id=9001的字段column_name为’fooU+1F4A9foo’时，虽然更新成功了，但出现了警告。当我们对当前表的该字段进行查询时，发现内容居然被截断了。因为这里的UTF-8并不是我们所认识的支持所有unicode的UTF-8。所以作为开发者我们需要记住，应该使用utf8mb4来对数据库的编码进行设定，而不是使用utf-8.这种截断在某些场景下将导致很严肃的安全问题。

## 0x04 Hacking with Unicode

在做了这么多的铺垫之后，让我们来看一下真实生活当中存在的Hacking.

### UTF-16/UTF-32可能导致XSS问题（谷歌实例）

![原文图片](/media/2016/06/f7ba9035a56df18905fa006a5c59a646)

### Unicode将导致用户名欺骗问题

![原文图片](/media/2016/06/7705918b776c8031e1ca67f842179cea)

### Unicode换行符将导致问题XSS

![原文图片](/media/2016/06/826c7b22355d2836b2f0c70a15122d16)

### Mysql unicode截断问题，导致注册邮箱限制绕过

![原文图片](/media/2016/06/a85f73ac18b5a669ae73baf03282af16)

### Mysql unicode截断问题，导致WP插件被爆菊：

![原文图片](/media/2016/06/e94aefb3c644afab0963d45d95f02e1d)

### Unicode问题导致Stackoverflow的HTML净化器被绕过：

![原文图片](/media/2016/06/06688da7381768955ad4c32f2fa63769)

## 0x05 写在最后

如果你是一个JavaScript开发者，我觉得这是一个你不容错过的文章。文章中不但给出了一些在开发过程中很有可能碰到的问题，也给出了相应的解决方案。如果你是黑客，文章中给出的检验这种缺陷的方法也许可以给你带来不少的收获。至少Mysql的截断问题，在我看来还是十分有趣的。

## Replies
