0x00 前言
最近世界变化太大,在大浪潮的冲击下没有人可以幸免,对于我这代人从来没有这么近距离的观测过时代的转折点。不过好在,事情渐渐的明朗了起来,我对于未来的顾虑开始慢慢消散,所以回到学习上来,开始重新静下心来做自己那点微小的工作。
想起以前挖src的时候,自己也经常编写一些信息搜集的自动化脚本,总是一开始用的很舒服,但随着各种情况的出现,原本的脚本总是修修补补变得越来越臃肿直到最后要改变一个流程要花很多时间并且可能会引入一大堆不可控的bug。从那时候起我就一直想要一个可以编排的自动化src扫描器。记得是20年还是19年,知名开源蜜罐hfish的作者三斤开发了一个soar平台,一款基于图形化对流程进行编排的系统,这也是我第一次知道soar的概念。那时候我就在想,能不能通过这个平台我自己制作一个src扫描器呢?
那时候太懒了,拖拖拖,拖到我失业了在家闲着没事干了,终于最近开始重新拾起这个想法,于是把整个过程重新记录一下希望能有越来越多的人加入进来去优化这个平台。
欢迎大家加入赛博回忆录知识星球,我会在知识星球和微信群里更新我所关注的更多技术相关内容~
0x01 平台的设想
我看过不少的国人编写的src挖掘工具,很多工具都喜欢自成体系,比如我开发一个工具那么我就要成为一个all in one的工具,基本上都是在做集成,一个工具包含从子域名搜集、目录扫描、漏洞探测、漏洞利用等一条龙。说实话我不喜欢这一类的工具,我更喜欢一个好轮子,一个针对某个功能做到非常友好的、规范性的输入输出、配置性强的轮子,比如oneforall就专门完成子域名搜集,比如nmap就专门扫端口,比如dirsearch专门爆目录等等。因为在我看来虽然一个一个工具执行过去速度没有集成来的快,但是他的灵活性会高很多,如果我对于某个集成工具的功能不满意想要做修改,那么这个修改成本就会高很多。 因此我就在想,有没有一个平台把每一个原子性的轮子工具通过编排的能力根据自己的想法对其进行组织执行。 先抛开具体的技术实现,我对于平台的整体结构大概分为以下部分:
-
编排引擎,负责对各个应用进行编排并提供一些基本的流程处理能力 -
原子应用,用于实现某个单一的安全功能比如端口扫描、目录爆破等并且能用json格式进行输入输出 -
转换脚本,用于对每个应用之间的输入输出进行自定义转换的粘合脚本 -
数据处理,对原子应用产生的数据进行高层抽象提取运算处理 -
交互界面,对数据结果进行搜索、展示、操作的管理界面
从上面大概结构图可以看出来,其实核心还是编排引擎,我们要实现这样一个系统本质上就是基于一个良好的编排引擎进行改造。我这边通过国产的w5系统进行一些修改完成了一个编排扫描器的雏形。
这里给大家展示一下效果:
0x02 什么是w5
w5(https://w5.io/)是面向企业安全与运维设计的低代码自动化平台,可以让团队降低人工成本,提升工作效率。可以把代码图形化、可视化、可编排。让不同的系统,不同的组件通过 APP 进行封装形成平台能力,利用 Trigger 去自动化执行你构思的剧本。 这听起来很抽象,这里我用w5构建了一个简单的扫描流程,大家看一眼也就明白了。
通过上图我相信很多人看了就明白了,类似于图形化编程,通过拖动各个组件来构建整个扫描剧本,构建完成后点击执行,这个剧本就可以像一段编写完成的代码一样按照我们预想的流程一样进行执行。 这看起来非常优雅和美好,只要支持的组件够多拥有足够的自由度,平台足够稳定,确实可以使我们不用花费时间和精力去用python编写各种工具,只需要拖一拖通过编排就可以获得一个预期的扫描器,并且当我们有新需求的时候只需要对着图形修改一下就完成了。
0x03 w5的安装
这边简单介绍一下安装与使用,当然w5的官方网站也写了基本的安装和使用教程,考虑到官网的步骤还是有一些坑的所以这边挑重点再讲一遍。 首先是安装,官方提供了源码安装和docker安装方式,虽然docker安装的方式非常方便,但在实际使用时我发现docker在运行一些高并发底层网络操作时可能会出现bug,所以这边建议不要使用docker来进行安装。 我这里把w5分成三个部分来安装:w5主体代码、mysql、redis
一、mysql和redis
mysql和redis我们可以通过docker的方式直接安装这样会方便很多 这里把w5安装在用户目录下
# 下载 W5,进入到 Docker 目录,需要配置 Mysql
git clone https://github.com/w5teams/w5.git && cd w5/docker
# 下载 Mysql,如果使用自己的 Mysql 服务,无需安装
docker pull mysql
# 启动 Mysql,name 为 W5_MYSQL,Nysql 初始化大概需要 5-10 秒左右
docker run -d --name W5_MYSQL -p 3307:3306 -e MYSQL_ROOT_PASSWORD=w5_12345678 -v $PWD/sql:/docker-entrypoint-initdb.d -v $PWD/conf.d:/etc/mysql/conf.d -v $PWD/mysql_db:/var/lib/mysql mysql
# 下载redis镜像
docker pull redis
# 启动redis
docker run -d -name W5_REDIS -p 6379:6379
二、w5本体部署
把mysql和redis装好后就可以直接采用官网的《linux部署》(https://w5.io/help/introduction/start.html#%E6%8B%89%E5%8F%96%E4%BB%A3%E7%A0%81)的操作方式来操作
1)安装依赖
# 默认国外源,国外服务器使用
pip3 install -r requirements.txt
# 使用国内源,国内服务器使用
pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
2)修改配置文件
在w5应用目录执行vi config.ini
,redis没有密码无需填写
[mysql]
host = 127.0.0.1
port = 3306
database = w5_db
user = root
password = w5_12345678
[redis]
host = 127.0.0.1
port = 6379
database = 0
password =
3)运行项目
执行python3 run.py
4)访问w5
访问地址: ip:8888, (访问不了请检查服务器防火墙) 账号密码: 账号:admin,密码:12345678 (登录后请及时修改密码)
如果你需要长期运行那么可以参考官网(https://w5.io/help/introduction/start.html#%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E9%83%A8%E7%BD%B2) 说的使用Supervisor来进行管理,这边就不赘述了。
0x04 包装一个原子应用
w5里已经提供了一些基础的app来供我们使用,比如它提供了一个nmap的app
图里的nmap已经是我改过的应用,因为其原有的nmap应用非常受限制所以我直接把它改了,这边基于这么一个nmap应用来讲一讲如何构建一个应用。这些内容基本上在官网也有写,我这边会进行一些简单的扩展。 我们可以进入到w5的目录里,然后找到apps目录,进入该目录我们可以看到每个应用其实就是一个目录
一般一个应用里会包含几个文件app.json icon.png main readme.md
这几个文件是不可或缺的,具体文件用处可以参考官网。 这里我对原app.json进行修改(官网的实例带有注释这会引起错误,json文件里需要去掉注释):
{
"identification": "w5soar",
"is_public": true,
"name": "nmap-X",
"version": "0.2",
"description": "一款端口扫描工具,已去除特征",
"type": "安全扫描",
"action": [
{
"name": "端口扫描",
"func": "scan"
}
],
"args": {
"scan": [
{
"key": "target",
"type": "text",
"required": true
},
{
"key": "args",
"type": "text",
"required": false,
"default": "-sV --version-intensity 0 -Pn"
}
]
}
}
我们再把main/run.py改一下:
#!/usr/bin/env python
# encoding:utf-8
# cython: language_level=3
from loguru import logger
async def scan(target, args):
try:
import nmap
except:
logger.info("[nmap] 导入 python-nmap 模块失败, 请输入命令 pip install python-nmap")
return {"status": 2, "result": "缺少 python-nmap 模块,请 pip install python-nmap 安装"}
logger.info("[nmap] APP执行参数为:{target}{args}", target=target, args=args)
nm = nmap.PortScanner()
#args = '-T4 -sV'
#if protocol == 'udp':
# args = '-sU --privileged'
nm.scan(hosts=target, arguments=args)
return {"status": 0, "result": nm.analyse_nmap_xml_scan()['scan']}
最后我们再把readme.md文件修改一下,把具体的参数说明和返回json格式实例给加入进去,这样再实际使用时我们可以通过查看说明文档来获得输入输出的具体格式参考。
我们看一下重新包装过的nmap-X应用
输入输出的使用说明文档
这样一个应用就包装完了。
0x05 转换脚本
w5并没有提供一个合适的转换脚本功能来处理两个应用输入输出的粘合能力,这一点很大程度上削弱了它的实际使用灵活性。这边我通过编写一个json处理应用来实现这个功能。这个应用的功能很简单,通过定义inputjson和outputjson两个变量然后通过输入parsercode来对inputjson进行处理后然后把结果赋予outputjson
比如一个简单的json2json的转换代码
async def json2json(inputjson,parsercode):
inputjson = json.loads(inputjson)
outputjson = {}
exec(parsercode)
result = outputjson
print_results = json.dumps(result)[:1000] + "n .........."
html = '''<span style="color:red">{print_results}</span>'''.format(print_results=print_results)
return {"status":0,"result":result,"html": html}
这里直接通过exec将用户输入的parsercode内容作为python代码进行执行 app.json文件内容:
{
"identification": "w5soar",
"is_public": true,
"name": "jsonparser",
"version": "1.2",
"description": "用于自定义json数据转换,inputjson是输入变量,outputjson是输出变量",
"type": "数据处理器",
"action": [
{
"name": "读取json文件,结果列表写入文件",
"func": "jsonfile2rowfile"
},
{
"name": "读取json文件,json结果直接返回",
"func": "jsonfile2json"
},
{
"name": "读取json,处理后返回json",
"func": "json2json"
}
],
"args": {
"jsonfile2rowfile": [
{
"key": "inputjsonfilepath",
"type": "text",
"required": true
},
{
"key": "rowfilename",
"type": "textarea",
"required": true
},
{
"key": "parsercode",
"type": "textarea",
"required": true
}
],
"jsonfile2json":[
{
"key": "inputjsonfilepath",
"type": "text",
"required": true
},
{
"key": "parsercode",
"type": "textarea",
"required": true
}
],
"json2json":[
{
"key": "inputjson",
"type": "text",
"required": true
},
{
"key": "parsercode",
"type": "textarea",
"required": true
}
]
}
}
最后的效果:
这样我们就完成了“转换脚本”这个部分的能力,这个json处理应用我回头会上传到w5的app市场上供给大家下载和使用。
0x06 数据入库
数据入库其实也很简单,我们也可以通过改造一个专门的数据入库的app来实现这个功能,这里我直接改造w5自带的mysql数据库应用。 app.json
{
"identification": "w5soar",
"is_public": true,
"name": "mysql-X",
"version": "1.4",
"description": "一个常用的关系型数据库管理系统,集成了扫描库的一些基本数据操作",
"type": "数据引擎",
"action": [
{
"name": "新增项目",
"func": "addProject"
},
{
"name": "查询",
"func": "query"
},
{
"name": "增删改",
"func": "update"
}
],
"args": {
"addProject": [
{
"key": "host",
"type": "text",
"required": true,
"default": "@{host_ip}"
},
{
"key": "port",
"type": "number",
"required": true,
"default": 3307
},
{
"key": "user",
"type": "text",
"default": "root",
"required": true
},
{
"key": "passwd",
"type": "text",
"default": "w5_12345678",
"required": true
},
{
"key": "db",
"type": "text",
"default": "cyber_scan",
"required": true
},
{
"key": "projectName",
"type": "text",
"required": true
},
{
"key": "domains",
"type": "textarea",
"required": true
},
{
"key": "projectDescription",
"type": "textarea",
"required": true
}
],
"query": [
{
"key": "host",
"type": "text",
"required": true,
"default": "@{host_ip}"
},
{
"key": "port",
"type": "number",
"required": true,
"default": 3307
},
{
"key": "user",
"type": "text",
"default": "root",
"required": true
},
{
"key": "passwd",
"type": "text",
"default": "w5_12345678",
"required": true
},
{
"key": "db",
"type": "text",
"default": "cyber_scan",
"required": true
},
{
"key": "sql",
"type": "textarea",
"required": true
}
],
"update": [
{
"key": "host",
"type": "text",
"required": true,
"default": "@{host_ip}"
},
{
"key": "port",
"type": "number",
"required": true,
"default": 3307
},
{
"key": "user",
"type": "text",
"default": "root",
"required": true
},
{
"key": "passwd",
"type": "text",
"default": "w5_12345678",
"required": true
},
{
"key": "db",
"type": "text",
"default": "cyber_scan",
"required": true
},
{
"key": "sql",
"type": "textarea",
"required": true
}
]
}
}
通过添加每一个操作当作一个数据模型,比如这里加了一个addProject
如果你有其他的动作其实也是一样的,最终效果如图:
0x07 流程粘合
前面讲了应用的编写,这边简单介绍一下常见的流程编排操作。
1)获取一个应用的输入输出
在w5里每一个app都通过uuid进行标识,一个app要想获取到另一个app的输入输出可以通过@(uuid.变量)
的形式来获取。 这里演示一下如何在oneforall的应用里调用数据库应用里的变量值
鼠标移动到app上,点击下图的地方复制uuid
比如这里我要获取这个mysql应用的domains的输入给到后面oneforall应用里使用,那么我只需要这样
这样就可以调用其他app里的数据了。 获取上一个app的输出也是类似的,只不过需要使用这样的方式@(uuid.result)
这里result获取到的是一个json格式的输出,如果你需要获取json里的某一个字段,那么只需要通过语法!>
来获取。比如上面的oneforall的输出结果的json格式是这样的
有一个results_file_path字段我需要使用。那么组合在一起就是: @(uuid.result!>results_file_path)
2)for循环和条件判断
w5还支持循环和条件判断
这里通过for循环遍历一个字典,然后把每一个遍历元素给到if分支进行参数判断,判断符合则进入后续流程,不符合则跳过流程。 for循环:
if模块:
3)剧本变量
w5还支持设置剧本的变量,比如某个值在全局都会被使用,那么我们可以通过变量的方式来调用,如果哪天需要修改时只需要修改变量值就可以全局应用了。这里我设置了一个主机ip的变量供给剧本里的app来使用。
通过语法@{变量名}
来使用
4)webhook和定时器
webhook和定时器是w5提供的另一个剧本调用的能力。定时器其实就和crontab类似是一个定时任务,如果我们在一个剧本里使用了定时器,那么系统本身就会定时对该剧本进行执行。
具体就不赘述了,大家自己使用试试就知道了。 同样的webhook也是一种剧本调用方式,只不过是通过web请求来调用罢了。
当我们对接口/api/v1/w5/webhook
发送json格式的post请求时,那么webhook的app就可以获取到请求的内容并触发对应的剧本进行执行。比如我们改一下原先的剧本为
然后我们将后一个数据库app的参数获取改为从webhook的app里获取
最后我们使用python发送post请求:
# 其他语言请自行百度
import json
import requests
headers = {
"Content-Type": "application/json"
}
data = {
"key": "C201690C0011E4CBF21E8E482EABD50B",
"uuid": "980b90b0-36d9-11eb-846f-dde86bba0048",
"text": "guahao.comnguahao.cn"
# 或者 "data": {"a":"b"}
}
r = requests.post(url="http://localhost:8888/api/v1/w5/webhook", headers=headers, data=json.dumps(data))
print(r.json())
这样我们就可以把域名列表通过webhook传递给剧本了。
0x08 通过AI强化低代码
在整个编排过程中我发现大部分时间都是浪费在转换脚本的编写上,一旦我陷入了脚本编写的工作里那么这么一个平台又似乎不如直接用python写一个扫描器来的方便些。 最近openAI火爆和它展示出强大的“需求->代码”的转换能力正好完美的解决了这个问题。由于json转换通常是一个比较简单的代码流程所以只要我们的需求描述够准确AI就不太会写出错误的代码。那么通过AI来自动生成转换脚本的想法就很自然的产生了。 比如我给AI一个需求如下:
inputjson={'107.36.113.181': {'hostnames': [{'name': 'wwwww.io', 'type': 'user'}], 'addresses': {'ipv4': '101.36.113.187'}, 'vendor': {}, 'status': {'state': 'up', 'reason': 'syn-ack'}, 'tcp': {21: {'state': 'filtered', 'reason': 'no-response', 'name': 'ftp', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}, 22: {'state': 'open', 'reason': 'syn-ack', 'name': 'ssh', 'product': 'OpenSSH', 'version': '8.0', 'extrainfo': 'protocol 2.0', 'conf': '10', 'cpe': 'cpe:/a:openbsd:openssh:8.0','servicefp':'', 'method':''}, 80: {'state': 'open', 'reason': 'syn-ack', 'name': 'http', 'product': 'nginx', 'version': '1.14.1', 'extrainfo': '', 'conf': '10', 'cpe': 'cpe:/a:igor_sysoev:nginx:1.14.1','servicefp':'', 'method':''}, 2222: {'state': 'filtered', 'reason': 'no-response', 'name': 'EtherNetIP-1', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}, 3306: {'state': 'filtered', 'reason': 'no-response', 'name': 'mysql', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}, 8081: {'state': 'filtered', 'reason': 'no-response', 'name': 'blackice-icecap', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}}}}
outputjson={}
将inputjson转换成outputjson并满足以下要求:
1. outputjson有一个元素为message,其值为一个列表
2. 取inputjson里每一个元素的key,对其里面的子元素里state为open的元素的key,将两个key通过冒号进行连接
3. 第二条里获取到的数据每一条作为output里message列表的一个元素
满足上面要求写出对应的python代码
他给出的结果:
确实牛逼,和我预期的一样,我们再来试试另一个复杂一点的转换需求。
inputjson={'107.36.113.181': {'hostnames': [{'name': 'wwwww.io', 'type': 'user'}], 'addresses': {'ipv4': '101.36.113.187'}, 'vendor': {}, 'status': {'state': 'up', 'reason': 'syn-ack'}, 'tcp': {21: {'state': 'filtered', 'reason': 'no-response', 'name': 'ftp', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}, 22: {'state': 'open', 'reason': 'syn-ack', 'name': 'ssh', 'product': 'OpenSSH', 'version': '8.0', 'extrainfo': 'protocol 2.0', 'conf': '10', 'cpe': 'cpe:/a:openbsd:openssh:8.0','servicefp':'', 'method':''}, 80: {'state': 'open', 'reason': 'syn-ack', 'name': 'http', 'product': 'nginx', 'version': '1.14.1', 'extrainfo': 'aaahttpaaa', 'conf': '10', 'cpe': 'cpe:/a:igor_sysoev:nginx:1.14.1','servicefp':'', 'method':'probed'}, 2222: {'state': 'filtered', 'reason': 'no-response', 'name': 'EtherNetIP-1', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}, 3306: {'state': 'filtered', 'reason': 'no-response', 'name': 'mysql', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}, 8081: {'state': 'filtered', 'reason': 'no-response', 'name': 'blackice-icecap', 'product': '', 'version': '', 'extrainfo': '', 'conf': '3', 'cpe': '','servicefp':'', 'method':''}}}}
outputjson={}
将inputjson转换成outputjson并满足以下要求:
1. outputjson有一个元素为message,其值为一个列表
2. inputjson里的每一个元素叫主元素,state为open,servicefp或extrainfo或product包含http关键词的,method为probed的元素叫子元素
3. 满足条件2的子元素,组合一个msg字典{"ip":主元素的key,"port":子元素的key,"service":子元素的product}
4. 将每一个msg作为outputjson里message列表的一个元素
满足上面要求写出对应的python代码
AI给出的答案
真牛逼,和我预期完全一样。 那么基本上只要我们语文够好,能给出测试用的输入用例,就可以通过AI直接生成一个python代码了。这样的代码大体上是没有太大问题的,可能在一些细节上我们还会根据实际情况进行少量修改,这样基本可以加速我们来编写转换脚本的过程。 当然我们还可以通过Copilot来实现类似的AI编写代码的效果,这边就不赘述了。
0x09 w5的优化建议
这边再给三斤关于w5的一些优化建议:
-
可以提供一个专门的转换脚本的官方APP或者功能,规范输入输出的格式 -
提供转换脚本市场,可以通过下载他人给出的脚本来直接使用 -
提供剧本市场,可以通过下载他人给出的剧本来直接使用 -
提供更好用的并发执行能力
0x10 结语
其实这样的平台国内也有不少人在尝试,但一直都难以推广,通过我自己使用下来的观感来看还是基础工作需要做的太多了,从规范统一到丰富的app市场,如果没有一个团队进行持续性开发并加上一个活跃的社区提供丰富的脚本、剧本、app,普通人使用这个平台来从0开始搭建自己的扫描器其实还是很费力的,可能还不如通过python直接写来的方便些。 不过我觉得这种平台是趋势,加上现在AI的兴起,通过这种方式不断的构建一个基础设施后,最终我们可能可以通过AI直接生成一个任意流程的扫描任务。这个想象空间还是很大的,非常有意思,至于能不能挖到漏洞,我倒不是很care,XD
原文始发于微信公众号(赛博回忆录):通过咒语和可编排实现低代码扫描器