在使用ARL(Asset Reconnaissance Lighthouse資產偵察燈塔系統,項目地址地址爲https://github.com/TophantTechnology/ARL)的時候,有兩個問題比較困擾我:
1. ARL使用Fofa導入數據的時候怎麼降重?
2. 如何自己手動編寫Poc?
在網上查閱了一些相關資料後,我發現並沒有有師傅寫的很清晰,於是誕生了寫這篇文章的想法。
這篇文章不涉及ARL的基礎搭建過程和基礎使用過程,如果您之前沒有使用過ARL,詳情可以參考官網教程:https://tophanttechnology.github.io/ARL-doc/system_install/
1.Fofa降重
先說結論,是由於Fofa_api的限制而不是ARL本身的問題
來源於我之前的使用體驗,使用同樣的Fofa語句,比如能搜到大量地的資產,但是ARL只會跑幾千條,然後我們反覆運行發現得到的資產結果是一致的,這樣就大大地影響了配合Fofa使用好處,只能自己更換不同的Fofa語句來實現降重,非常麻煩。
首先我們先黑盒看看調用fofa的流程:
POST /api/task_fofa/test HTTP/2
{"query":"org=\"China Education and Research Network Center\""}
HTTP/2 200 OK
{"message": "success", "code": 200, "data": {"size": 13492282, "query": "org=\"China Education and Research Network Center\""}}
可以看見這裏返回的結果是13492282條
然後我們直接去項目裏面去找:
路徑爲:ARL-2.6.1\app\routes\taskFofa.py
from flask_restx import Namespace, fields
from app.utils import get_logger, auth, build_ret, conn_db
from app.modules import ErrorMsg, CeleryAction
from app.services.fofaClient import fofa_query, fofa_query_result
from app import celerytask
from bson import ObjectId
from . import ARLResource
ns = Namespace('task_fofa', description="Fofa 任務下發")
logger = get_logger()
test_fofa_fields = ns.model('taskFofaTest', {
'query': fields.String(required=True, description="Fofa 查詢語句")
})
@ns.route('/test')
class TaskFofaTest(ARLResource):
@auth
@ns.expect(test_fofa_fields)
def post(self):
"""
測試Fofa查詢連接
"""
args = self.parse_args(test_fofa_fields)
query = args.pop('query')
data = fofa_query(query, page_size=1)
if isinstance(data, str):
return build_ret(ErrorMsg.FofaConnectError, {'error': data})
if data.get("error"):
return build_ret(ErrorMsg.FofaKeyError, {'error': data.get("errmsg")})
item = {
"size": data["size"],
"query": data["query"]
}
return build_ret(ErrorMsg.Success, item)
add_fofa_fields = ns.model('addTaskFofa', {
'query': fields.String(required=True, description="Fofa 查詢語句"),
'name': fields.String(required=True, description="任務名"),
'policy_id': fields.String(description="策略 ID")
})
@ns.route('/submit')
class AddFofaTask(ARLResource):
@auth
@ns.expect(add_fofa_fields)
def post(self):
"""
提交Fofa查詢任務
"""
args = self.parse_args(add_fofa_fields)
query = args.pop('query')
name = args.pop('name')
policy_id = args.get('policy_id')
task_options = {
"port_scan_type": "test",
"port_scan": True,
"service_detection": False,
"service_brute": False,
"os_detection": False,
"site_identify": False,
"file_leak": False,
"ssl_cert": False
}
data = fofa_query(query, page_size=1)
if isinstance(data, str):
return build_ret(ErrorMsg.FofaConnectError, {'error': data})
if data.get("error"):
return build_ret(ErrorMsg.FofaKeyError, {'error': data.get("errmsg")})
if data["size"] <= 0:
return build_ret(ErrorMsg.FofaResultEmpty, {})
fofa_ip_list = fofa_query_result(query)
if isinstance(fofa_ip_list, str):
return build_ret(ErrorMsg.FofaConnectError, {'error': data})
if policy_id and len(policy_id) == 24:
task_options.update(policy_2_task_options(policy_id))
task_data = {
"name": name,
"target": "Fofa ip {}".format(len(fofa_ip_list)),
"start_time": "-",
"end_time": "-",
"task_tag": "task",
"service": [],
"status": "waiting",
"options": task_options,
"type": "fofa",
"fofa_ip": fofa_ip_list
}
task_data = submit_fofa_task(task_data)
return build_ret(ErrorMsg.Success, task_data)
def policy_2_task_options(policy_id):
options = {}
query = {
"_id": ObjectId(policy_id)
}
data = conn_db('policy').find_one(query)
if not data:
return options
policy_options = data["policy"]
policy_options.pop("domain_config")
ip_config = policy_options.pop("ip_config")
site_config = policy_options.pop("site_config")
options.update(ip_config)
options.update(site_config)
options.update(policy_options)
return options
def submit_fofa_task(task_data):
conn_db('task').insert_one(task_data)
task_id = str(task_data.pop("_id"))
task_data["task_id"] = task_id
task_options = {
"celery_action": CeleryAction.FOFA_TASK,
"data": task_data
}
celery_id = celerytask.arl_task.delay(options=task_options)
logger.info("target:{} celery_id:{}".format(task_id, celery_id))
values = {"$set": {"celery_id": str(celery_id)}}
task_data["celery_id"] = str(celery_id)
conn_db('task').update_one({"_id": ObjectId(task_id)}, values)
return task_data
其中有一個類和倆函數在其他地方:
# -*- coding:UTF-8 -*-
import base64
from app.config import Config
from app import utils
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
class FofaClient:
def __init__(self, email, key, page_size=9999):
self.email = email
self.key = key
self.base_url = Config.FOFA_URL
self.search_api_url = "/api/v1/search/all"
self.info_my_api_url = "/api/v1/info/my"
self.page_size = page_size
self.param = {}
def info_my(self):
param = {
"email": self.email,
"key": self.key,
}
self.param = param
data = self._api(self.base_url + self.info_my_api_url)
return data
def fofa_search_all(self, query):
qbase64 = base64.b64encode(query.encode())
param = {
"email": self.email,
"key": self.key,
"qbase64": qbase64.decode('utf-8'),
"size": self.page_size
}
self.param = param
data = self._api(self.base_url + self.search_api_url)
return data
def _api(self, url):
data = utils.http_req(url, 'get', params=self.param).json()
if data.get("error") and data["errmsg"]:
raise Exception(data["errmsg"])
return data
def search_cert(self, cert):
query = 'cert="{}"'.format(cert)
data = self.fofa_search_all(query)
results = data["results"]
return results
def fetch_ip_bycert(cert, size=9999):
ip_set = set()
logger.info("fetch_ip_bycert {}".format(cert))
try:
client = FofaClient(Config.FOFA_EMAIL, Config.FOFA_KEY, page_size=size)
items = client.search_cert(cert)
for item in items:
ip_set.add(item[1])
except Exception as e:
logger.warn("{} error: {}".format(cert, e))
return list(ip_set)
def fofa_query(query, page_size=9999):
try:
if not Config.FOFA_KEY or not Config.FOFA_KEY:
return "please set fofa key in config-docker.yaml"
client = FofaClient(Config.FOFA_EMAIL, Config.FOFA_KEY, page_size=page_size)
info = client.info_my()
if info.get("vip_level") == 0:
return "不支持註冊用戶"
# 普通會員,最多隻查100條
if info.get("vip_level") == 1:
client.page_size = min(page_size, 100)
data = client.fofa_search_all(query)
return data
except Exception as e:
error_msg = str(e)
error_msg = error_msg.replace(Config.FOFA_KEY[10:], "***")
return error_msg
def fofa_query_result(query, page_size=9999):
try:
ip_set = set()
data = fofa_query(query, page_size)
if isinstance(data, dict):
if data['error']:
return data['errmsg']
for item in data["results"]:
ip_set.add(item[1])
return list(ip_set)
raise Exception(data)
except Exception as e:
error_msg = str(e)
return error_msg
誒?我看到這個代碼的時候我感覺他這裏寫的沒有毛病,那麼我的疑問變得更重了,爲什麼我查詢的結果明明有一千萬多條,但是實際到我們的項目條數就幾千條:
conn_db('task').insert_one(task_data)
根據上面這條代碼,於是我進入了docker容器,查看了數據庫的具體信息:
大致數據就是下面這種格式:
"fofa_ip" : [ "xxx.xxx.xxx.xxx", ...... ], "celery_id" : "xxx", "statistic" : { "site_cnt" : 3935, "domain_cnt" : 0, "ip_cnt" : 2590, "cert_cnt" : 0, "service_cnt" : 0, "fileleak_cnt" : 3068, "url_cnt" : 0, "vuln_cnt" : 94, "npoc_service_cnt" : 0, "cip_cnt" : 0, "nuclei_result_cnt" : 0, "stat_finger_cnt" : 167, "wih_cnt" : 0 } }
這裏對應的fofa_ip數量與我在前端上面看到的數量一致,我就納悶了爲什麼這裏就只有這麼幾千條IP數量呢?我準備把這部分的代碼手動抽出來在我本地上跑一遍試試看看結果到底是什麼?
【----幫助網安學習,以下所有學習資料免費領!加vx:dctintin,備註 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
我在本地運行後發現返回的ip數量是由page_size
決定的,如下面的代碼
def fofa_query_result(query, page_size=9999):
try:
ip_set = set()
data = fofa_query(query, page_size)
if isinstance(data, dict):
if data['error']:
return data['errmsg']
for item in data["results"]:
ip_set.add(item[1])
return list(ip_set)
我將page_size改爲20000,發現根本不返回結果了,這裏我纔想起來回到Fofa_API的官網去查看,發現了是API一次只能返回最多10000條的限制。
那麼解決辦法是什麼呢?
我們可以看到Fofa Api這裏存在一個翻頁參數,我們的改進措施就是讓ARL使用Fofa API的時候增加一個翻頁參數而不是不添加導致每次都是第一頁。面向大衆的話,要我們一個一個去修改源代碼是不太現實的,我這裏將給原作者發起一個issue
,期待他的更新。
最簡單的措施就是我們改進一下Fofa語句:
(status_code="200" || banner="HTTP/1.1 200 OK") && org="China Education and Research Network Center"
避免過期資產誤殺:
這裏是有20萬多條獨立IP,我們可以利用Fofa_api先把這20多萬條獨立IP下載下來,使用ARL本身的添加任務功能將這些IP填進去,這樣的缺陷就是不能跑Poc,要跑Poc的話可以等待我們這20多萬的數據跑完一遍後,然後直接風險任務下發選擇對應的Poc就可以了。
添加任務的時候使用下列的格式加入:
IP1
IP2
IP3
IP4
IP5
IP6
...
2.Poc編寫
想要優雅地使用ARL,會自己編寫更新Poc是必不可少的。
ARL的poc工具在路徑/opt/ARL-NPoC/xing/plugins/poc
中我們後續在這個路徑去修改,我們可以從作者的倉庫看看這個工具:https://github.com/1c3z/ARL-NPoC
這裏需要單獨注意一點,我們在安裝的時候
pip3 install -r requirements.txt
這裏最後一個PyYAML
直接安裝會報錯,我們直接使用下列命令直接安裝。
pip install PyYAML
然後安裝運行:
pip3 install -e .
就可以使用了:
大致使用教程:
這裏我拿直接ARL給我掃出來的一個弱口令進行驗證:
xing brute -t 目標地址
然後我們就可以開始編寫Poc了
我們來分析一個較爲簡單的但是很實用的Actuator API 未授權訪問
漏洞的POC:
from xing.core.BasePlugin import BasePlugin
from xing.utils import http_req
from xing.core import PluginType, SchemeType
class Plugin(BasePlugin):
def __init__(self):
super(Plugin, self).__init__()
self.plugin_type = PluginType.POC # 定義該插件的類型便於後續調用
self.vul_name = "Actuator API 未授權訪問"
self.app_name = 'Actuator'
self.scheme = [SchemeType.HTTPS, SchemeType.HTTP]
def verify(self, target):
paths = ["/env", "/actuator/env", "/manage/env", "/management/env", "/api/env", "/api/actuator/env"]
for path in paths:
url = target + path
conn = http_req(url)
if b'java.runtime.version' in conn.content:
self.logger.success("發現 Actuator API 未授權訪問 {}".format(self.target))
return url
主要流程就是先定義一個插件的類,然後使用函數__init__(self)
寫出這個插件的一些信息,具體實現過程在verify函數
中實現。
這裏我就編寫一個influxdb的未授權訪問的漏洞:
from xing.core.BasePlugin import BasePlugin
from xing.utils import http_req
from xing.core import PluginType, SchemeType
class Plugin(BasePlugin):
def __init__(self):
super(Plugin, self).__init__()
self.plugin_type = PluginType.POC
self.vul_name = "Influxdb未授權訪問"
self.app_name = 'Influxdb'
self.scheme = [SchemeType.HTTPS, SchemeType.HTTP]
def verify(self, target):
url = target + "/query?q=SHOW%20USERS"
conn = http_req(url)
if b'"results":' in conn.content:
self.logger.success("發現 Influxdb 未授權訪問 {}".format(self.target))
return url
else:
return False
然後我們直接在本地復現一下,是可以使用的:
接着我們部署到我們的服務器上,注意這裏我們將POC同步到arl_web和arl_work兩個容器中:
大致流程就是分別進入這兩個容器然後添加對應文件下的Poc即可:
cd /opt/ARL-NPoC/xing/plugins/
我們更新一下Poc後再前端也可以查看到了:
然後經過測試確實可以:
3.總結
在我的實際滲透測試過程中,ARL給我的信息蒐集帶來了很大的便利性。是一種全面的信息蒐集的有力方式!這篇文章主要是解決一點使用ARL過程中的問題,以及編寫自己的Poc的流程。
更多網安技能的在線實操練習,請點擊這裏>>