小改ARL

大伙应该都听说过斗象科技的资产灯塔系统(ARL),我是在大一的时候认识这个平台的,印象中当时跑这个平台,2核2G的云服务器是相当稳的。但是随着它的更新,现在2核2G的云服务器已经基本跑不动了,基本上跑一会就会内存爆炸,现在官方给的配置建议是4核8G的云服务器,这对于我们新手来说太不友好了。于是我就把它后端铲了重构

首先明确一下对于我这种散兵使用ARL的需求,作为一名msf忠实拥护者、cobalt strike单机版玩家,我需要的是小巧精准的单兵工具,然而目前的ARL平台向着规模化、集群化的方向发展,已然违背了我这种散兵对众测场景下大规模资产收集的需求。毕竟to B的挣钱潜力大概还是比to C要猛的,任何一个资产扫描平台都应该向着企业的需求,这样才能赚钱;如果一个开发者想赚逆向大爷的钱,那他的软件只能被破解

一句话总结一下,我的需求就是;只要不爆内存,能跑就行

改之前做个梦

要想实现一个大规模的平台,事先的设计论证是很重要的,首先要想清楚这个事情我能不能做、我有没有经验(以前有什么值得参考的最佳实践)、我想做成什么样、我分成几步做、然后画个架构出来,我知道大伙有时候会下午喝咖啡的,晚上睡不着的时候就可以在床上比划这个

其实我没有比划过我这个“小改ARL”平台的架构,因为我一开始就打算小改

首先我们理解一下ARL原版的架构,其实ARL的架构不复杂,只需要看看分布式版本的ARL部署文档就能明白,它的任务分发全是由web容器控制rabbitmq进行的,扫描结果则是由worker容器直接写入mongodb数据库

1713631767319.png

到这里我给大伙算算,worker容器里面,一个celery进程跑嗨了之后吃300mb(可能是它的celery跑ARL的代码跑出300mb占用的,并不是celery框架本身肥),默认情况下跑两个celery,就是600mb;然后web容器至少跑3个gunicorn进程处理http请求,这个gunicorn吃多少内存我忘了,因为它存在感比较低,偶尔见过几次依稀记得一个进程30mb左右吧;rabbitmq 200mb内存;mongodb 200mb左右

加起来差不多1.1G左右吧

阿里云2G的服务器,去掉公摊实际到手1.7G,系统+docker可能要吃掉300mb,剩下1.4G可用空间。除了ARL这1.1G的占用,还有那些第三方工具,这种情况下nuclei是肯定不能跑了,甚至ARL自己跑phantomjs进行网站截图等操作都可能会OOM

现在我们了解了ARL的架构以及内存占用,那么我们要怎么做自己的架构呢?

答案是不用做,直接抄!

抄!可不是一模一样的抄,我们知道ARL的肥点是那个celery和rabbitmq,那这两个东西说白了就是队列调度模块,我们既然决定不做分布式了,这个队列就可以砍了,砍成自己写的;然后python语言的并发那些东西不如golang好用,所以

根据抄写ARL的调度模式,我们可以设计一个这样的架构

1713692451040.png

主要不一样的地方就是web不直接写消息队列,因为golang的队列类似物是内存保存的,虽然实现磁盘保存也不是不能做,但是会比较麻烦,所以就直接干数据库里了

任务管理器每隔五秒就会取一个任务塞队列里,只有工作线程(社畜)从队列里取走任务后,任务管理器才会塞下一个任务。这种设计在断电时可能会丢失队列里和社畜手上的任务,但是问题不大,因为数据库的任务数据还在,所以到时候检查一下看看哪些任务卡住了,直接重启即可

北纬 17.2k 优选工具

ARL的扫描代码我大致看了一下,感觉里面集成的很多功能都是自研的;比较好用的工具包括npoc、wih,这些我直接抢过来用。不过我实在是不忍心使用那个phantomjs,因为人家都停止更新了,还是放过它吧

于是根据ARL原版工序和自己拍脑袋的深思熟虑,我构造了一个完整的社畜工作流程

  1. 域名解析:把域名解析成IP
  2. 端口扫描:调用nmap扫描IP开放的端口
  3. 站点识别:让httpx访问每个端口,进行站点截图、站点技术识别、网页icon hash获取操作
  4. 服务识别:调用npoc进行端口的服务识别
  5. 敏感信息收集:调用web info hunter进行敏感信息收集
  6. 漏扫:调用npoc进行未授权访问、弱口令等漏扫
  7. 文件泄露扫描:调用nuclei进行默认口令、敏感文件泄露等扫描

大伙可能会发现我没有进行子域名收集,因为我考虑到子域名收集是一个多维度的工作,所以目前我更想自己做这个活,所以就没把这个功能做进去

这中间还有很多调优的工作,比如说有的站上了CDN,可能开了300个端口,但是300个端口只有一个是站点,其他端口都会响应422状态码,因此就需要做一系列的操作将这些无效站点去除。我直接参考ARL的代码直接把这些“老师傅的经验之谈”抄过来了,不得不说人活在这世上,还是不用动脑过得更舒服,至少脑子舒服了

总结与展望

做完了之后感觉挺牛逼的,一跑起来我成傻笔了,nuclei真的是内存大胃王,直接把我干到OOM,然后到github上面去看关于OOM的issue,官方的态度就是“我不是我没有你不要乱说”,但是提issue的哥们不畏强权,最后证明是某些个poc导致内存占用异常离谱;后来调查结果发现,是爆破相关的poc造成了大量的并发请求,而nuclei为每个请求分配了大约10mb的缓冲区,然后内存炸了;现在nuclei会检测系统整体的内存使用情况,如果内存占用率较高,会自动降低并发数

OOM killed when using the nuclei SDK with the standard templates · Issue #4756 · projectdiscovery/nuclei (github.com)

因为我httpx和nuclei都是直接调用sdk,调用SDK有许多好处,好处之一就是有完整的报错,开发调式更方便;好处之二就是输出格式固定,不需要费劲巴拉的去解析输出文件格式,而且有的工具输出文件的格式还不固定,这种就很折磨

虽然nuclei已经对内存占用有一定的优化,但是它对内存的需求还是比较离谱的;上面的链接里面,官方的意思说,如果要减少nuclei的内存占用,可以从并发数、同时扫描的目标数、加载的poc数量等方面入手优化,于是我优化了一下nuclei的配置,好歹现在跑4-5个小时不会OOM了

目前的性能情况是这样的,我分几个情况概述一下:

  1. 待机情况下:主进程70mb、mongodb 150mb
  2. 跑nmap:主进程90mb、nmap 50mb * 2、 mongo不变
  3. 跑httpx:主进程270mb、chrome浏览器 150mb-300mb、 mongo不变
  4. 跑npoc:主进程不变、npoc 50mb
  5. 跑wih:wih跑太快了,看不到
  6. 跑nuclei:主进程400mb-900mb、 mongo不变、快OOM的时候似乎会触发垃圾回收,每次能回收个100mb-300mb

目前我的“小改ARL平台”还处于稳定性观察期,先跑个edusrc看看,后续使用如果还出现OOM的问题,我就再对架构进行改进,现在我已经拍脑袋想好了改进方案了,大伙看图

1713708044669.png

这是整个worker模块的重新设计图,对应总架构图的“社畜1”、“社畜2”部分,该模块在初始化时需要进行4个协程的初始化:前序工作组、httpx执行器、中序工作组、nuclei执行器;和4个管道的初始化:httpx任务管道、中序任务管道、nuclei任务管道、并发限制器

并发限制器是一个chan结构,chan的容量和配置文件中设置的并发数相匹配,当每个协程开始工作之前,都要向并发限制器写入一个interface{}、工作完之后,要从并发限制器里面取出一个interface{},这样就能保证全局的并行任务数不超过用户的限制,并且整个进程里面只有一个httpx和一个nuclei在运行

不过这个结构有点小复杂,改起来有点麻烦,以后再说