1. 项目缘起:为什么我需要一个AWD作战管理台
先说结论:AWD(Attack With Defense,攻防兼备)比赛不是一个人能撑起来的游戏,但很多队伍实际打起来,却常常变成“一个人扛三路”的局面。
我在打CTF的第三年,第一次完整经历了一场8小时的AWD线下赛,那场记忆特别深刻。队友们各有各的活法:有人在疯狂种菜(种WebShell)、有人盯着流量在洗日志、还有人在后台手忙脚乱地补漏洞。问题是,各干各的,完全串不到一起。我们手里有大概12台靶机,分布在不同网段,每台机器上开了什么服务、被种了几个后门、哪些账户被改过密码、哪个Flag还没读——全靠一张Excel表和微信群里的碎片信息。打到第五个小时,有个队友给一台靶机改了MySQL密码,另一台马上连不上了,两台机器之间还互相怀疑是被对手动了手脚。整场下来,防守端漏读了好几个Flag,进攻端种下的后门也有一半莫名其妙失效了。
赛后复盘的时候我就在想,AWD的核心痛点根本不是某个漏洞怎么打、某个系统怎么防,而是“多目标、多操作、多状态”下的管理混乱。一台机器你可以手动维护,5台勉强靠记忆,但10台以上的机器同时在线上,攻击窗口又只有几秒钟,没有一套统一管理的手段,整个队伍就是一团散沙。
后来断断续续踩了大半年坑,终于搞明白了一件事:AWD比赛里最需要的东西,不是更犀利的exp,而是一个能把“查看状态、维持权限、快速加固、稳定读Flag”这四件事捏合在一起的平台化工具。这也是我想写这篇文章的初衷——把我在实际比赛中做这套AWD管理台的经验完整梳理一遍,从思路到落地,从踩坑到补救,尽量说透。
什么人适合看这篇文章?如果你正在准备第一次线下AWD、队伍里三四个人但没人愿意记靶机密码,或者你想把平时练习赛的攻防节奏提上来,这篇文章都能给你一套可以直接抄的作业。如果你是老手,后面关于权限轮换与流量对抗的部分,也可能给你一些你之前没注意过的细节。
2. 一站式管理:先把目标清单变成一张“活地图”
2.1 最初的痛点:Excel表根本救不了AWD
AWD比赛里,大家有个很常见的误区:以为管理就是记地址、记账号密码。实际上,AWD赛场的管理难点在于三个“动态”:
- 目标的动态性:赛方会在某个时间点重置某些靶机,或者突然开放新的攻击区,你的清单必须能实时同步;
- 权限的动态性:你种下的后门可能被对手删掉,你自己也可能为了修补漏洞把WebShell误杀了,状态必须能随时刷新;
- Flag的动态性:每轮评分的Flag会变化,每个目标可能同时存在多个Flag点,这些信息如果靠人脑记,第五轮之后基本就是灾难现场。
我见过最惨烈的案例是,某个队伍打到最后两轮,手里拿着曾经拿到的所有账号密码挨个尝试登录,结果没一个能连上。不是对手把密码改了,是他们自己人在中途做安全加固的时候,把所有账号都轮换了,但没同步到清单里。
所以这个管理台的第一性原则就是:所有关键信息必须集中、实时、可追溯。地址、账号、密码、后门位置、Flag读取情况、漏洞修复状态,全部统一登记,并且任何一次操作都要有操作留痕。
2.2 我采用的管理结构:基于目标为中心的数据模型
设计思路上,我没有把它做成传统数据库里那种“一张大表”,而是按“目标 = 房间”的方式组织信息。每个目标下属多个数据集。
| 数据集 | 说明 | 例子 |
|---|---|---|
| 基础信息 | IP、端口、系统类型、中间件 | 192.168.3.21,Linux,Nginx 1.18 |
| 权限信息 | 系统账号、数据库账号、后门位置与类型 | root / xxxxx, MySQL root / yyyy, /var/www/html/css/.config.php |
| 加固状态 | 已修复漏洞、已禁用功能、已改密码项 | 改掉MySQL弱口令,删除eval注入点 |
| Flag状态 | Flag路径、读取状态、当前Flag值(如需要) | /flag.txt,已读取,flag{xxx} |
| 操作日志 | 谁在什么时间做了什么 | 23点11分 队友B 重置了SSH公钥 |
这个模型的好处是:每个目标的信息内聚在一起,新增目标就像开了一个新房间,不会出现“账号在表A、Flag在表B、加固记录在表C”这种割裂状态。实际操作的时候,我维护了目标状态总览页和详情页两层视图,总览页用红黄绿三色标注健康度:绿色代表完全可控、黄色代表部分可控(比如还能拿到权限但不确定有没有被留后门)、红色代表已经完全失守。
2.3 管理台本身的实现思路
因为是比赛场景,我不建议搞重客户端或者复杂的数据库后端。一个轻量级的Web面板就足够,我用的是Flask + SQLite + 前端模板这套组合。选它的理由很朴素:
- Flask足够小,单文件就能启动,丢在任何一个跳板机上就跑起来了;
- SQLite不需要额外部署数据库服务,文件即库,重启不丢数据;
- 前端直接用模板渲染和Ajax刷新,不需要打包构建,安全风险面也缩小。
具体到页面,核心就是三个:目标总览(卡片列表)、目标详情(含所有数据集的编辑表单)、操作日志页。为了多人协作,我在前端加了轮询刷新,每隔5秒拉一次最新状态,避免队友之间互相覆盖信息。这里有个容易被忽略的点:写入操作需要用简单的锁机制。我用的是一个毫秒级时间戳hash作为提交token,同一个目标同一时间只允许一个操作进行,防止两个人同时改密码导致状态错乱。
2.4 管理台功能的额外加分项:批量操作
AWD比赛中最容易让人崩溃的,是同一件事要在十几台机器上重复做。比如修复一个通用漏洞、批量轮换密码、批量上传WebShell。管理台如果没有批量操作能力,那“一站式”就名不副实。
我在管理台里打通了批量命令通道:选中多个目标后,可以一键下发SSH命令(通过paramiko)或HTTP请求(Web类目标)。批量操作的设计要点在于分阶段确认:
- 选择目标和目标上的具体操作类型(加固、改密、读Flag、上传文件等);
- 批量生成可预览的命令文本,先在一台机器上试跑;
- 确认无误后批量执行,结果回写至每个目标的操作日志。
听起来不难,但实际比赛里,批量操作翻车概率挺高的。我遇到过把一台机器上正确的Shell路径替换掉了,结果批量下发到所有机器,全部连不上。后来加了第二道保险——每个目标的配置都做了“目标指纹”校验,下发前先比对目标指纹(系统版本、中间件路径、关键文件hash),指纹不匹配就拒绝下发,很大程度降低了操作误伤。
3. 权限维持:后门不是种得越多越好,而是越隐蔽越好
3.1 权限维持的本质是什么
在AWD比赛里,权限维持(简称权限维持,有些人叫“留后路”)决定了你后续每一轮能不能持续拿到Flag。但很多新手对这个词的理解停留在“多埋几个马”,这种思路在早期的CTF里还行,现在的裁判和防守系统早就不是这个段位了。
我举两个常见的场景:
场景A:你上传了一个PHP一句话木马到Web目录,然后用蚁剑连上了。对手开局五分钟扫描全目录,把所有常见文件名(shell.php、1.php、upload.php)全部删掉了。你的权限维持宣告失败。
场景B:你把一个不显眼的后门藏在了图片文件里,配合了一个小马加载器。对手扫了三天没发现,但你每个轮次都稳定读取Flag。这就是权限维持的意义。
二者的差别不是“后门数量”,而是后门的隐蔽性、稳定性和可控性。
3.2 常见后门类型与对抗思路
AWD里常用后门基本可以分四类:
| 类型 | 实现方式 | 优点 | 风险 |
|---|---|---|---|
| Web类后门 | PHP/JSP一句话木马、重写正常文件、图片马 | 使用简单,配合客户端可互操作 | 易被扫描删除,容易被流量检测 |
| 系统类后门 | SSH后门账号、公钥注入、cron定时器 | 稳定,不易清理 | 对手可能在线程列表发现异常进程 |
| 内存类后门 | 修改程序启动参数、注入内存Shell | 极隐蔽 | 靶机重启后丢失 |
| 影子类后门 | 创建隐藏账号、修改文件属性 | 穿透性强 | 系统级检查时容易暴露 |
我的经验是:至少保留两条不同类型的后门。一条放在Web层,负责日常读取Flag;一条放在系统层,负责兜底。万一Web层的马被清了,系统层后门还能让你直接拿到权限,重新种上新的马。
3.3 权限维持实操:三条不同路线的配置记录
我实际在比赛中用过的几种稳定方案,按推荐程度排序给你参考。
第一方案:SSH公钥后门 + 隐藏作者。拿到一台Linux靶机的root权限后,第一时间把公钥写入/root/.ssh/authorized_keys,并且把时间戳调成和系统原有文件保持一致(touch -r)。这种方式走的是系统原生通道,不产生额外进程,隐蔽性非常强。但要小心:不要在lastlog或wtmp留下登录痕迹,最好配合脚本清理。
第二方案:伪静态WebShell。在一个正常的JS文件或CSS文件末行追加一句话,再用一个正常的入口文件(比如首页)做加载。这种马通常需要配合一个“密码”参数才能激活,平时看起来就是个普通的资源文件,扫描器一般不会去检测这类文件的内容。不过要注意,如果对手做了全站文件hash对比,伪造的文件会在hash层面露馅,因此尽量选择不会被覆盖的静态页面。
第三方案:计划任务反弹型后门。在cron中写入一个远程拉取命令,每5分钟从你的VPS拉一次payload并执行。这个方案稳定性极好,但前提是你有一个比赛期间稳定的控制端主机。省赛线下赛一般网络隔离,VPS可能连不上,这种方案只适用于线上赛或没有严格Egress限制的赛制。
3.4 权限维持中我吃过的大亏
说一个我踩过的大坑。有一场比赛,我用了一个自写的PHP马,密码拆成了三段,分别存在三个不同的环境变量里。当时觉得很稳,连了四轮都没问题。结果第五轮对手做了全目录扫描,把所有包含base64_decode的文件全删了——我的马是功能完整型,直接躺枪。更惨的是,我系统层的后门用的是一把静态公钥,对手通过流量分析锁定了我的SSH指纹,直接把我控制端IP封了,后门还在但我进不去。
事后总结两点教训:
- 不要用“通用特征”太明显的代码,比如
eval($_POST['x'])里的“eval”和“POST”,经典得不能再经典。尽量用拆分函数、绕过检测的写法,甚至用assert配合数组变形,比直接用eval安全得多。 - 权限维持的可用性必须定期自测。比赛每隔一段时间,我会用控制脚本远程执行一条无害命令(比如
id)来确认后门活没活着。如果不回显,就要立刻启动备用通道去补救。
4. 基线加固:不是“关掉所有漏洞”,而是“让对手无处下嘴”
4.1 为什么不能盲目加固
基线加固(Baseline Hardening)这个词,在AWD圈子里被很多人等同于“把所有漏洞补上”。这个理解其实很危险。你要知道,AWD比赛的得分逻辑和实战渗透不同——你修补漏洞是为了让攻击方从你这里拿不到分,而不是为了让你的系统变成一台完美的服务器。因此,一味追求“绝对安全”会带来三个负面后果:
- 浪费时间:有些漏洞修补需要写复杂的规则或改源码,投入产出比极低;
- 影响业务连续性:你把Web服务的某些函数禁用后,网站功能可能直接挂了,反而让裁判误判你已经宕机,扣分更狠;
- 破坏后门可用性:你修复了一个漏洞,但实际上这个漏洞正是你后门的入口。比如你为了防RCE,把
system()函数禁掉了,结果自己的后门也跑不动了。
所以,正确的加固思路是:在保证服务可用性的前提下,快速抢占关键要害,缩小攻击面,并且确保自己留下的后门仍然畅通。
4.2 基线加固的分级操作表
实战中我建议把加固任务分成三优先级去执行。下表是我每次比赛都会过一遍的检查清单。
| 优先级 | 加固项 | 具体操作 | 预期效果 |
|---|---|---|---|
| P0 | 数据库弱口令 | 修改MySQL/Redis默认账号密码,使用强口令(20位以上混合字符) | 防止对手读库取Flag、修改配置 |
| P0 | Web目录命令注入 | 自定义WAF规则过滤恶意参数,禁用危险函数 | 阻断对手直接通过Web执行命令 |
| P0 | 系统SSH弱口令 | 修改root口令、关闭密码登录改公钥登录 | 防止爆破上机 |
| P1 | 常见后门路径 | 检索敏感目录,删除常见马名(shell.php等) | 清理对手留下的低级后门 |
| P1 | 中间件配置泄漏 | 关闭目录列表、禁用调试信息 | 减少信息泄露 |
| P2 | 全站文件hash备份 | 对Web目录做一次性快照,便于对比恢复 | 快速发现对手篡改文件 |
| P2 | 日志定期清空或压缩 | 及时清理自身操作痕迹,避免暴露后门位置 | 降低审计风险 |
实际操作的时候,不要试图一次性执行所有项。我的习惯是开局前5分钟只做P0和P1,P2放到第一轮判分之后再从容处理。因为开局阶段对手进攻最猛,你如果没有最基本的口令防线,几秒钟就会被拿下。
4.3 通过管理台批量加固的实践记录
这里说一个我实际用管理台批量加固的案例。有一场比赛,我们10台靶机都是同一套应用系统,也就是说,只要修好一台的通用问题,其他9台可以完全复制修补方案。我在管理台里做了这么一套操作:
- 选定一台“样机”,手动完成全部加固动作,记录执行过的命令和修改过的文件;
- 用管理台生成一份“加固模板”,包含脚本内容和目标列表;
- 先对第二台目标做小范围试运行,观察是否影响Web服务正常响应;
- 确认无误后,把模板批量下发到剩余机器,同时记录每台机器的执行hash值。
那次比赛我们一共用了不到15分钟,就把10台机器的口令和Web漏洞全部处理完了。对比旁边队伍手动一台一台去改,效率差距非常明显。
不过也有翻车的时候。有一场比赛,我们批量改SSH口令时,其中一台机器由于SSH配置文件和别的不一样(赛方有微调),导致批量命令执行失败,而且SSH服务直接重启了,队伍里没人知道那台机器变成什么样了。后来我才发现,批量操作必须加“指令执行结果复核”模块——执行完必须回传输出并比对预期结果,不能被“执行成功”这种假象骗了。
4.4 加固过程中的自救备份策略
做基线加固时,最怕的其实不是加固失败,而是加固过程中误删了关键文件。我建议每个参赛队伍在比赛前准备一个“应急恢复包”:里面包含基础Web使用的原始页面备份、关键二进制文件、数据库初始化脚本。一旦把自己服务器搞挂了,最快5分钟就能恢复。
我当时的管理台里放了一个“Web目录快照”功能,备份策略是:开局拿到靶机权限后,立刻对Web目录做一次全量压缩打包,并把压缩包存到一个不受Web映射影响的目录或直接拉回控制端。这样无论后续怎么折腾,你永远有一份“开局原版”能回滚。这一点在后面对抗激烈时特别救命——你以为对手把页面删了,其实只是覆盖了,但你有初始备份,恢复过去还能保持服务正常。
5. Flag读取:自动化、准确率、去重,一个都不能少
5.1 为什么读Flag会成为瓶颈
很多人以为,AWD比赛最难的环节是攻击,读Flag有什么难的?你拿到权限后,直接在目标机器上执行cat /flag不就完了?
话是这么说,但实际比赛中读Flag有四个隐藏问题:
- Flag路径不固定:每台靶机的Flag可能在
/flag、/flag.txt、/tmp/flag、数据库某个表里,甚至可能在内存里。你手动去找,一轮下来可能都找不齐。 - Flag值每轮变化:AWD的Flag通常是动态的,每轮替换一次。你如果在第3轮手动读了一次,第4轮Flag变了,就不会再自动更新,容易错失得分。
- 读取方式不能暴露后门:如果你的读Flag操作直接触发敏感命令,比如执行了
eval()或system('cat /flag'),对手可能通过安全设备记录到你的后门位置,然后顺手清掉。 - 多目标并发:十台机器同时要读Flag,人根本忙不过来。
解决这些问题的唯一办法,是把读Flag做成一个自动化的轮询脚本,适配不同的获取方式,并且保持低调。
5.2 多路径Flag读取的自动化配置
我的管理台里专门做了一个“Flag分发器”模块,它按照目标类型自动选择读取方式。这里给出一个我常用的自动化逻辑:
def read_flag(target): # 1. 根据目标的权限类型选择读取通道 if target.perm_type == "web": result = call_webshell(target, "cat /flag") elif target.perm_type == "ssh": result = ssh_exec(target, "cat /flag*.txt 2>/dev/null; cat /flag 2>/dev/null") elif target.perm_type == "db": result = sql_query(target, "select flag from flag_table limit 1;") # 2. Flag格式检测与去重 pattern = r"flag\{[^}]+\}" matches = re.findall(pattern, result) if matches: # 3. 写入到该目标的历史记录里,比对是否新Flag update_flag_history(target.id, matches[-1]) return matches[-1] else: # 4. 找不到时触发手动告警 notify_manual(target.id, "Flag not found") return None需要注意的是,cat /flag这种命令会暴露你的后门行为,所以我通常会在WebShell执行时把命令“伪装”成从请求参数传递的加密数据,让流量侧不容易直接看到明文命令。比如先把cat /flag编码进特定UA头,再在后门侧解码执行。这样安全设备截图也不会直接看到cat /flag字样的特征。
5.3 读取频率与评分的协同策略
AWD比赛的判分机制通常是:每轮(比如2分钟或5分钟)评分时,如果你在目标上维持读取Flag的能力,且读到了正确的当前轮次Flag,就给分。所以你的读取频率最好能跟评分轮次对齐,但别太快,太快容易被发现。
我实际验证过的稳妥节奏是:每轮评分开始前10-15秒自动触发一次读取,而不是每5秒就扫一次。这样的好处有三个:
- 减少流量层面的异常特征,对手做流量审计时更难发现;
- 避免频繁触发目标服务器上的资源消耗,防止靶机卡顿;
- 与轮次评分时间对齐,确保你读到的一定是“最新一轮”的Flag。
管理台里,我给每个目标都配了一个“读Flag定时器”,可以根据比赛轮次自动调整触发时间。一开始我用的固定5秒轮询,后来发现赛后裁判日志里,某个目标被访问了近千次,异常流量特征非常明显。改成对准轮次再读后,整体流量形态就自然多了。
5.4 读Flag过程中的安全禁区
这里必须说几条我亲测过的雷区。
雷区一:不要在WebShell里直接执行系统命令来读Flag。很多队伍的WebShell页面里有“命令执行”功能,一眼可见,对手只要扫到你的WebShell页面,就知道这是你的后门入口。正确做法是让后门只接受加密指令。
雷区二:不要依赖单一读取通道。比如你只用一个PHP马,如果该路径被禁用或被对手做了文件锁,你就读不了。至少准备Web和SSH两个通道做自动切换,或者把数据库通道作为兜底。
雷区三:不要将明文Flag直接打印到Web页面。曾经有队伍为了省事,直接让WebShell把Flag回显在HTTP响应里,结果被对手的流量监控直接截取,甚至据此反推了后门参数。正确做法是让Flag回传走加密通道,比如用AES加密后再拼接固定前缀返回。
6. 踩坑实录与技术复盘
6.1 常见问题排查速查表
我整理了一份高频率出现的故障对照表,基本覆盖了AWD管理台使用中的大多数问题:
| 现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 管理台页面刷新后状态丢失 | 后端SQLite写入异步失效 | 检查写入操作是否commit,查看日志报错 | 所有写操作强制显式commit |
| 批量下发命令部分机器失败 | 目标机系统版本差异或路径不一致 | 逐台比对系统指纹和文件路径 | 下发前增加指纹匹配校验 |
| SSH后门无法连接 | 赛方重置了authorized_keys | 检查目标机登录日志 | 改用cron反弹通道或Web通道 |
| WebShell路径被删 | 对手全局扫描 | 检查文件是否存在,查Web日志 | 预置备用隐藏路径,自动恢复 |
| Flag读取不到值 | 目标Flag路径不在预设清单中 | 手动上机确认位置 | 更新该目标的Flag路径清单 |
| 管理台IP被封 | 流量特征过于明显 | 检查访问频率和UA头 | 降低轮询频率,使用随机化UA |
这张表不是万能的,但它能帮你把“问题定位”的时间从半小时压缩到五分钟。真正打比赛时,每一分钟都很值钱,不要在排查上墨迹。
6.2 一个典型的连环故障实录
我想分享一场省赛中的真实经历。比赛开始时,我们队伍的第一梯队负责读Flag,我负责平台维护。开局四轮都挺顺利,到了第五轮,突然有三台目标机的WebShell全部失联。我一开始以为是赛方重置了靶机,后来通过系统层后门登录进去看,发现Web目录里的马全被一个陌生脚本删了——是某个对手写了一个自动化清理工具,专门匹配常见马的特征。
好在系统层后门没暴露,我又重新种了两个新马:一个用了自定义加密函数和伪静态后缀,另一个作为备用放在Js目录里。与此同时,我给管理台加了一个“自检告警”模块:每次读取Flag失败时,自动切换备用通道重试一次,如果还是失败,才向群里发通知。
那次之后,我再也没有出现过“所有后门同归于尽”的窘境。现在的比赛环境里,工具化攻击越来越普遍,如果后门方案没有冗余设计,被对手一次性打穿是大概率事件。
6.3 复盘总结:踩过的坑和长出的经验
把教训浓缩成几个字:冗余、适配、低调。
“冗余”是指后门和多通道一定要有Plan B;“适配”是指管理和操作流程要能适应不同赛制和不同目标环境;“低调”是指一切流量和行为都要尽量减少异常特征。
具体到日常练习,我会建议每个准备打AWD的人都做一次“管理台专项训练”:把三台虚拟机当靶机,一台当跳板,模拟5轮评分,测试自己在每轮中能否稳定完成“读Flag、加固、切换后门”三件事。这个训练看似简单,实际上能暴露很多细节问题:比如你手动登录靶机的时间是否超时、批量命令是否有误把自己锁在门外的风险、WebShell存活率是否足够。
我个人在实际操作中最大的体会是——工具永远是为了服务“决策效率”而存在的。AWD比赛的胜负手,往往不是谁的exp更锋利,而是谁能在混乱中更快地组织信息、更快地切换策略。把这个管理台做出来之后,我们队伍从“各管各的”变成了“一个平台调度所有人”,整体攻击节奏和防守稳定性都上了一个台阶。
如果你以后有机会参加AWD比赛,不妨先从这张Excel表的管理思维开始,然后逐步把“目标信息、权限通道、加固任务、Flag读取”塞进一个工具里。哪怕不用我这套技术方案,只要理解了“信息集中和流程自动化”这两个核心逻辑,你在线下赛场里就已经比一半的队伍稳了。