跳到主要内容

常见攻击类型

1. XSS

首先说下最常见的 XSS 漏洞,XSS (Cross Site Script),跨站脚本攻击,因为缩写和 CSS (Cascading Style Sheets) 重叠,所以只能叫 XSS。

XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。

大致细分为几种类型。

非持久型 XSS

非持久型 XSS 漏洞,也叫反射型 XSS 漏洞,一般是通过给别人发送带有恶意脚本代码参数的 URL,当 URL 地址被打开时,特有的恶意代码参数被 HTML 解析、执行。

举一个例子,比如你的 Web 页面中包含有以下代码:

Select your language:
<select>
<script>
document.write(
'' +
'<option value=1>' +
location.href.substring(location.href.indexOf('default=') + 8) +
'</option>',
)
document.write('<option value=2>English</option>')
</script>
</select>

攻击者可以直接通过 URL (类似:https://xx.com/xx?default=) 注入可执行的脚本代码。

为了防止出现非持久型 XSS 漏洞,需要确保这么几件事情:

  • Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。

  • 尽量不要从 URL,document.referrer,document.forms 等这种 DOM API 中获取数据直接渲染。

  • 尽量不要使用 eval, new Function(),document.write(),document.writeln(),window.setInterval(),window.setTimeout(),innerHTML,document.creteElement() 等可执行字符串的方法。

  • 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。

  • 前端渲染的时候对任何的字段都需要做 escape 转义编码。

持久型 XSS

持久型 XSS 漏洞,也被称为存储型 XSS 漏洞,一般存在于 Form 表单提交等交互功能,如发帖留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。

主要注入页面方式和非持久型 XSS 漏洞类似,只不过持久型的不是来源于 URL,refferer,forms 等,而是来源于后端从数据库中读出来的数据。

为了防止持久型 XSS 漏洞,需要前后端共同努力:

  • 后端在入库前应该选择不相信任何前端数据,将所有的字段统一进行转义处理。

  • 后端在输出给前端数据统一进行转义处理。

  • 前端在渲染页面 DOM 的时候应该选择不相信任何后端数据,任何字段都需要做转义处理。

2. CSRF

CSRF(Cross-Site Request Forgery),中文名称:跨站请求伪造攻击

CSRF 原理

csrf

完成 CSRF 攻击必须要有三个条件:

  • 用户已经登录了站点 A,并在本地记录了 cookie

  • 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点 A)。

  • 站点 A 没有做任何 CSRF 防御

CSRF 的特点

  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。

  • 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。

  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。

  • 跨站请求可以用各种方式:图片 URL、超链接、CORS、Form 提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

预防 CSRF

根据 CSRF 的两个特点:

  • CSRF(通常)发生在第三方域名。

  • CSRF 攻击者不能获取到 Cookie 等信息,只是使用。

专门制定防护策略,如下:

  • 阻止不明外域的访问

    • 同源检测

    • Samesite Cookie

  • 提交时要求附加本域才能获取的信息

    • CSRF Token

    • 双重 Cookie 验证

3. SQL 注入

SQL 注入原理

下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。

考虑以下简单的管理员登录表单:

<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>

后端的 SQL 语句可能是如下这样的:

let querySQL = `
SELECT *
FROM user
WHERE username='${username}'
AND psw='${password}'
`
// 接下来就是执行 sql 语句...

目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,

可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang' OR 1 = 1 --,密码随意输入,就可以直接登入系统了。

思考一下能知道,我们之前预想的真实 SQL 语句是:

SELECT * FROM user WHERE username='zoumiaojiang' AND psw='mypassword'

可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式:

SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx'

在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了:

SELECT * FROM user WHERE username='zoumiaojiang' OR 1 = 1

这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号。

如何预防 SQL 注入

防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,

下面列出防御 SQL 注入的几点注意事项:

  • 严格限制 Web 应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害

  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。

  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

mysql.query(`SELECT * FROM user WHERE username = ? AND psw = ?`, [username, psw])
  • 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。

  • 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。

  • 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。

4. 命令行注入

命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例:

假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下:

// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec
let params = {
/* 用户输入的参数 */
}

exec(`git clone ${params.repo} /some/path`)

如果 params.repo 传入的是 https://github.com/huteming/huteming.github.io.git 当然没问题了。

可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就惨了。

预防命令行注入

防止命令行注入需要做到以下几件事情:

  • 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。

  • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。

  • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。

还是前面的例子,我们可以做到如下:

const exec = require('mz/child_process').exec

// 借助 shell-escape npm 包解决参数转义过滤问题
const shellescape = require('shell-escape')

let params = {
/* 用户输入的参数 */
}

// 先过滤一下参数,让参数符合预期
if (!/正确的表达式/.test(params.repo)) {
return
}

let cmd = shellescape(['git', 'clone', params.repo, '/some/path'])

// cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path
// 这样就不会被注入成功了。
exec(cmd)

5. 参考文章