终于轮到了日志注入
久仰日志注入大名,今天来分析分析。
1 |
|
冒号被过滤,不能用伪协议了。直接上日志注入。
先抓包看看响应头 server:
字段,确定是什么服务器软件。(浏览器 F12 也自带查看响应头的工具)
我抓出来是 server: nginx/1.20.1
,服务器为 nginx。上网查资料,nginx 日志文件位置为: /var/log/nginx/access.log
,一会儿要用。
大多数服务器日志会记录以下信息:
- 时间戳:请求发生的日期和时间。
- 客户端IP:访问者的IP地址。
- 请求方法:如
GET
、POST
、PUT
等。 - 请求路径:访问的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 |
|
此处可大写绕过与 php://input
相结合。
URL:
1 | /?file=Php://input |
请求体:
1 | <?php system("ls");?>//第一次请求 |
借此讲讲 php://input
怎么用。
php://input
是一个 PHP 内置的伪协议,用于读取 HTTP 请求中的原始数据。
从超级全局变量 $_POST
数组读取的是处理过的、作为一个个元素的参数。当需要读取 POST 原始数据时,通常就会用到 php://input
伪协议来读取请求体。因为是直接读取请求体,所以 HTTP method 不是 POST 也能读取。
所以,我们可以发送请求时抓包,然后修改请求体为 Payload ,并把 URL 中将要被文件包含的参数的值改为 php://input
,最后发包看看响应的效果。
多层函数绕过过滤
题目
1 |
|
不要被 \(|\)
吓住了,过滤的是中文括号。
直接看 payload 吧:
Payload1
1 | ?c=echo highlight_file(next(array_reverse(scandir(pos(localeconv()))))); |
解析:
localeconv()
返回包含本地化数字和货币格式信息的关联数组(有关各种货币相关符号与货币数据格式,有兴趣可自行找资料看看,本处只涉及小数点),**该数组的第一个元素通常是小数点.
**(如['.', ...]
),被我们借来表示当前目录。pos()
**别名current()
**,返回数组内部指针当前位置的值。PHP数组有一个隐藏的 “内部指针”,初始指向第一个元素。此处指向localeconv()
生成数组中的第一个元素.
,用于表示当前目录( Linux 知识点)。scandir('.')
扫描当前目录,返回按升序排列的文件列表,例如:['.', '..', 'flag.php', 'index.php']
。array_reverse()
反转数组顺序,得到:['index.php', 'flag.php', '..', '.']
。next()
将数组内部指针从第一个元素(index.php
)后移一位,返回第二个元素flag.php
。
关于数组指针操作函数,参考以下表格。
数组指针操作函数返回值规则函数 是否移动指针 返回值 current()
/pos()
否 当前指针位置的值(越界返回 false
)next()
是(后移) 新位置的值(越界返回 false
)prev()
是(前移) 新位置的值(越界返回 false
)reset()
是(重置) 数组第一个元素(空数组返回 false
)end()
是(跳末) 数组最后一个元素(空数组返回 false
)key()
否 当前指针位置的键名(越界返回 null
)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 |
|
与前面的题相比,没有了 eval()
函数,但可通过 data://
伪协议解决。
什么是 data://
伪协议:
data://
伪协议允许直接在 URI 中嵌入数据(如文本或 Base64 编码的内容),常用于动态生成数据流。
tips:
URI 统一资源标识符,URI 包括 URL 和 URN,或其他特殊形式,比如此处的 data://
伪协议。URL 通过协议、位置等标识资源;URN 仅通过名称标识资源。
人话:data://
伪协议相当于在 data://
后写入数据, data://
及其后的数据会被当作文件处理。
看看实例就明白了:
1 | $uri = 'data://text/plain;base64,SGVsbG8gV29ybGQh' |
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.ini
里short_open_tag
选项设置为On
时有效;<?= ?>
则无需配置即生效)将文件包含改为
include($c.".php");
的情况(往data://
数据的最后拼接了.php
)。其实只有<?php ?>
中的内容会被解析,文件中 PHP 代码以外的文本会直接回显到网页(例如,此处如果$c
要执行的命令无任何返回值,则网页上只会出现.php
,因为.php
在 PHP 代码以外)
文件包含与 php:// 伪协议
1 |
|
大量过滤,无法使用空格、分号、单引号和括号。
所以利用文件包含漏洞, 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
2include($_GET[1])
include $_GET[1]另外, PHP 解释器会把
include$_GET[1]
中的$_GET[1]
解析为变量而不会因为无空格就报错。?>
是干什么的?因为分号被过滤了,语句不完整会报错无法执行,所以要找分号的代替品。
?>
前的代码会被解析为 PHP 的最后一句代码,不用加分号也不会报错。此外,eval()
函数不是把 PHP 语句“插入”到代码中再执行,所以这里不会出现?>
把后面的代码全部截断的问题。为什么不直接写文件路径
./flag.php
而是写一长串php://
开头的东西?文件包含时:
- 若文件内容包含有效的PHP标签(如
<?php
开头),PHP会解析并执行标签内的代码,标签外的文本会直接输出。- PHP 解析器从
<?php
开始解析代码,直到遇到?>
或文件末尾。 - 如果文件末尾没有
?>
,解析器会直接执行到文件结束,不会报错,且所有代码均有效。
- PHP 解析器从
- 若文件内容没有PHP标签,PHP会将其视为纯文本或HTML,直接输出内容到前端。
剧透一下
flag.php
文件,这个文件有<?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://stdin
、php://stdout
、php://stderr
:访问标准输入、输出和错误流(常用于命令行脚本)。php://filter
:对数据流应用过滤器(如编码转换、压缩)。(本题所用方法)php://temp
、php://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
$flag="ctfshow{ead582c0-8247-4aa9-a2fa-abc1bb47c29d}";有
<?php
开头且语法正确,但作用仅仅是定义了一个变量$flag
,所以直接包含该文件会解析为 PHP 文件且无回显。转换为 Base64 编码,则被识别为文本文件,直接回显内容。
关于服务器的知识(持续增加)
访问的是 PHP 文件,为什么前端得到的是 HTML 文件
服务器收到客户端浏览器发来的请求后,会检查储存在服务器上的对应文件类型。
若发现文件是静态文件( .html/.css/.js
),则直接将文件返回给浏览器;
如果有动态内容(如 .php
),则发送给处理对应内容的插件生成内容,然后将内容返回给浏览器(如将请求转发给 PHP 解释器,然后返回处理结果)。
若服务器未安装生成对应动态内容的插件或模块,或未正确进行配置,则无法正常返回网页(如 Nginx 或 Apache 在没有配置 PHP 支持时,会对 .php
请求返回 403 Forbidden
、 404 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 |
|
在没有任何 JavaScript 代码的情况下,打开网页时有一弹窗,有两个输入框分别输入账号密码,下方有“登录”或“取消”两选项。该网页源代码没有任何涉及弹窗的内容,所以出现弹窗看起来很反常。
其实是利用了 HTTP Basic 认证协议:
当服务器返回
401 Unauthorized
状态码时,浏览器会强制自动弹出账号密码输入框。这是 HTTP 协议规定的标准行为,无需前端代码。用户输入的账号密码会被浏览器自动拼接为
username:password
格式,并进行 Base64 编码,最终添加到请求头的Authorization
字段中:1
Authorization: Basic base64(username:password)
上述
base64(username:password)
实际替换为账号、冒号与密码拼接成的字符串的 Base64 编码。Basic
是认证方式,表示 HTTP Basic Authentication (基本认证),还有Digest
、Bearer Token (Token-Based)
以及其他许多认证方式。
全过程由浏览器原生实现,不需要任何 JavaScript 代码。
URL 知识点
1 | https://www.example.com:8080/path/to/resource?query=param#fragment |
https://
协议(Scheme)。其他常见协议:
http
、ftp
等www.example.com
域名(Host/Domain)。
:8080
端口(Port)。默认端口如
80
、443
等可省略,非标准端口需要显式声明。/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
)
- 规则与查询参数类似,但