自动化漏洞猎人代码分析

news2024/11/18 9:45:29

0x00 前言

安全人员可以扫描,网络上悬赏网站等的漏洞,如果能够发现其存在着安全漏洞,则可以通过提交漏洞的方式来获得一定的赏金,国外的这类悬赏的网站比较多,比如hackone,这上面列出了大量的资产信息,白帽子们可以分析这些资产,发现漏洞来获取赏金。hackone上截至到2020年6月,已经有六名白帽子获得了百万奖金。

0x01 大概了解

hackone上的资产个数是非常多的,如果人工分析起来,不光累,而且效率还低,不靠谱,所以就有了很多自动化的工具。网上也有不少开源的,找了几个研究了下,大概的流程都差不多,无非是利用各种开源工具的组合来完成漏洞的探测工作。

一般的流程就是几步:

  1. 信息收集 : 收集资产信息、详细收集域名和服务端口等。

  2. 漏洞探测 Fuzz: 即用xray等工具扫描资产是否存在漏洞;

  3. 提醒功能 :  如果自动探测到了漏洞,要提醒我们记得提交,更灵活的工具,可能包含自动提交。

我主要研究的工具是AUTO-EARN :

  1. 这个工具比较简单,但是却五脏俱全,除了hackone上采集域名没有外,其他的都有,而且这个框架比较灵活,方便各个部分的工具的升级。

  2. 界面也很酷,终端界面+一个网页统计信息展示b59e2e353a01f9d45213e1b095ed4a83.png

747dc2c06fca867dbadbea6970d5d20a.png
扫描样例

0x03 执行方法

1.执行顺序:sh start.sh  -->  python3 autoearn.py  --> sh stop.sh2. start.sh 即:

chmod +x ./tools/crawlergo
chmod +x ./tools/xray/xray_linux_amd64 
nohup python3 server.py > logs/server.log 2>&1 &
nohup ./tools/xray/xray_linux_amd64 webscan --listen 127.0.0.1:7777 --webhook-output http://127.0.0.1:2333/webhook > logs/xray.log 2>&1 &
nohup python3 subdomain_monitor.py > logs/subdomain_monitor.log 2>&1 &

1、前面两个增加可执行权限就不说了,看看后面,启动server.py 来获取通知等信息。 2、我们把xray启动起来,并且开启代理端口,等爬虫将爬取的网页送过来扫描; 3、开启子域名执行情况的检测程序; 4、subdomain_monitor.py 检查子域名扫描结果,将数据保存到sqlite表中。

  1. python3 autoearn.py输入1 即进行子域名扫描;

  2. 要等到子域名扫描结束,再输入2进行端口检测、完成后输入3进行waf检测(可选)

  3. 最后输入5 进行爬虫爬取网页后输入到xray进行漏洞探测、探测到漏洞后会发通知。

0x04 流程分析

开源地址已经讲代码讲的非常详细了,昨天看了一天,基本上懂点python的就可以看的懂,感谢作者这么用心,整个框架利用众多安全处理工具,如下图:1b3df93516ab9caf287cbe9623e89944.png

工具介绍:

信息收集:
------------
1. OneForAll:功能强大的子域名收集工具,可以根据域名获取所有子域名信息,也算是个集合工具;
利用证书透明度收集子域名、利用爬虫收集域名、利用DNS收集子域名、利用威胁情报收集子域名、利用搜索引擎来收集子域名;
2. Shodan是个搜索引擎网站,这里面利用它来搜索IP开放的端口信息;
3. masscan+nmap都是用来探测IP开放端口的,前者速度更快,后者可以发现服务名;
4. wafw00f 探测waf指纹的工具,如果有waf,我们忽略这个目标。

Fuzz
--------------
1. crawlergo 作为爬虫爬取相关链接;
2. xray: 长亭开发的免费的安全检测模块,可以进行xss漏洞、SQL注入、命令注入、目录枚举、文件上传等;
3. Server 酱: 这个工具挺有意思,可以免费进行微信通知,免费版本一天最多通知五次;
4. 利用flask框架做个简单的展示页面,利用Echarts显示统计报表信息;
5. sqlite3 这个就是一个文件的简单DB。

处理的数据流:a8ae81245ee1d97df2de21bbcd231432.png

4.1 子域名收集

步骤说明:

  1. target.txt 里面每行保存一行域名信息,可以简单的改成从网站采集后处理。

  2. 第一个执行的插件是OneForALL,作用就是探测target中的子域名信息; 这里面是通过autoearn.py中的命令1来实现的,调用代码:

subdomain_collect.oneforall_collect(config.target_file_path)

由于获取子域名是个非常耗时的操作,启动的是后台进程再查看:

def oneforall_collect(target):
    cmd = 'nohup python3 ' + config.oneforall_path + ' --target ' + target + ' run > logs/oneforall.log 2>&1 &'
    try:
     rsp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
     console.print('正在后台进行子域收集', style="#ADFF2F")
    except:
        console.print('子域收集失败,请检查输入格式', style="bold red")

收集子域名放在后台后,我们需要知道什么时候执行完毕,可以通过tail -f logs/oneforall.log 可以查看执行的日志,收集完子域名后,将结果放入到OneForAll下面的reuslt下面的一个sqlite数据库里面:

如果收集的域名为:example.com 则:
example_com_origin_result        表存放每个模块最初子域收集结果。
example_com_resolve_result      表存放对子域进行解析后的结果。
example_com_last_result            表存放上一次子域收集结果(需要收集两次以上才会生成)。
example_com_now_result            表存放现在子域收集结果,一般情况关注这张表就可以了

观测到的日志信息:

04:51:14,588 [ALERT] utils:252 - GET http://114.55.181.28/check_web/databaseInfo_mainSearch.action?isSearch=true&searchType=url&term=5nine.com&pageNo=1 404 - Not Found 5042
04:51:14,589 [INFOR] module:65 - The WZPCQuery module took 1.1 seconds found 0 subdomains
04:51:14,613 [ALERT] utils:252 - GET https://searchdns.netcraft.com/ 403 - Forbidden 7540
04:51:14,617 [INFOR] module:65 - The NetCraftQuery module took 1.2 seconds found 0 subdomains
04:51:14,861 [ERROR] module:118 - (MaxRetryError('HTTPSConnectionPool(host=\'www.search.ask.com\', port=443): Max retries exceeded with url: /web?q=site%3A.5nineservice.demo.5nine.com&page=1 (Caused by SSLError(SSLError("bad handshake: SysCallError(104, \'ECONNRESET\')")))'),)
04:51:14,862 [INFOR] module:65 - The AskSearch module took 1.3 seconds found 0 subdomains
OneForAll is a powerful subdomain integration tool
             ___             _ _ 
 ___ ___ ___|  _|___ ___ ___| | | {v0.3.0 #dev}
| . |   | -_|  _| . |  _| .'| | | 
|___|_|_|___|_| |___|_| |__,|_|_| git.io/fjHT1

OneForAll is under development, please update before each use!

[*] Starting OneForAll @ 2023-06-24 06:24:41

07:13:05,615 [INFOR] oneforall:249 - Finished OneForAll

检测程序,不光会检测子域名是否收集完成,还会将其插入到sqlite表中,进行后续的流程,核心代码在通知的server_push.py中:

# 子域收集状态提醒
def subdomain_status_push():
    console.log('子域收集完成')
    sql_connect.task_sql_check()
    sql_connect.subdomain_sql_check()
    sql_connect.vuln_sql_check()
    sql_connect.insert_subdomain_sql(sql_connect.oneforall_results_sql())
    subdomain_num = len(sql_connect.read_subdomain_sql())
    content = """``` 子域收集结束```
#### 结果:  共收集到了{subdomain_num}个子域
#### 发现时间: {now_time}
""".format(subdomain_num=subdomain_num, now_time=time.strftime("%Y_%m_%d_%H_%M_%S", time.localtime()))
    try:
        resp = requests.post(config.sckey,data={"text": "子域收集完成提醒", "desp": content})
    except:
        console.print('子域提醒失败,请检查sckey是否正确配置', style="bold red")

关键代码在于:   sql_connect.insert_subdomain_sql(sql_connect.oneforall_results_sql())即读取oneforall的扫描结果插入到一个子域名的表中:

# 读取OneForAll数据库
def oneforall_results_sql():
    url_result = []
    oneforall_conn = sqlite3.connect(config.oneforall_sql_path)
    console.print('OneForAll数据库连接成功',style="#ADFF2F")
    oneforall_c = oneforall_conn.cursor()
    oneforall_cursor = oneforall_c.execute("select name from sqlite_master where type='table' order by name;")
    for table_name in oneforall_cursor.fetchall():
        table_name = table_name[0]
        if 'now' in table_name:
            sql_cmd = "SELECT subdomain from " + table_name
            oneforall_c.execute(sql_cmd)
            for url in oneforall_c.fetchall():
                url = url[0]
                url_result.append(url)
    oneforall_conn.close()
    return url_result

由于oneforall是一个域名建一个表的,我们将表里面的子域名信息都集合起来,然后插入到子域名表中:

# 插入SUBDOMAIN数据库
def insert_subdomain_sql(url_result):
    subdomain_conn = sqlite3.connect(config.result_sql_path)
    console.print('AUTOEARN数据库连接成功',style="#ADFF2F")
    subdomain_c = subdomain_conn.cursor()
    for url in url_result:
        now_time = time.strftime("%Y_%m_%d_%H_%M_%S", time.localtime())
        try:
            subdomain_c.execute("INSERT INTO SUBDOMAIN (URL,SUBDOMAIN_TIME) VALUES ('%s', '%s')"%(url,now_time))
            subdomain_conn.commit()
        except:
            console.print('插入子域数据库失败',style="bold red")
    console.print('插入子域数据库成功',style="#ADFF2F")
    subdomain_conn.close()

4.2 端口扫描

端口检测阶段,端口扫描是通过查询子域名表,即SUBDOMAIN 核心代码是在port_check.py中的这个函数:

def mul_subdomain_port_check(threadName, q):
    url_list = []
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            domain = q.get()
            queueLock.release()
            try:
                if len(check_cdn.check_cdn(domain[1])) == 1:
                    url_list.extend(shodan_port_check(check_cdn.check_cdn(domain[1])[0],domain[1]))
                else:
                    console.print('目标存在CDN', style="bold red")
                    url_list.append('http://'+domain[1])
            except:
                console.print('目标' + domain[1] + '查询异常', style="bold red")
            console.print("%s processing %s" % (threadName, domain[1]), style="#ADFF2F")
        else:
            queueLock.release()
    sql_connect.insert_task_sql(url_list)

在利用shodan检测端口的时候,需要先进行cdn检测,如果域名对应的ip超过1个,则说明含有cdn,直接加入域名进行后续检测,不通过shodan检测。

url_list.extend(shodan_port_check(check_cdn.check_cdn(domain[1])[0],domain[1]))关于cdn检测,代码不复杂,但是没做过还是不容易想到.即根据利用socket.getaddrinfo来获取ip列表 信息,再保存起来,多个ip就认为有CDN(这里面还可以优化下)。

# 判断CDN函数
def check_cdn(domain):
    ip_list = []
    try:
        console.print('正在进行CDN检测', style="#ADFF2F")
        addrs = socket.getaddrinfo(domain, None, family=0)
        for item in addrs:
            if item[4][0] not in ip_list:
                if item[4][0].count('.') == 3:
                    ip_list.append(item[4][0])
                else:
                    pass
        return ip_list
    except:
        console.print('CDN检测失败,请检查输入格式', style="bold red")
        pass

扫描到端口后进入下一步是进行端口探测,是通过:sql_connect.insert_task_sql(url_list)将数据插入到任务表,插入任务表代码如下:

# 插入TASK数据库
def insert_task_sql(url_result):
    task_conn = sqlite3.connect(config.result_sql_path)
    console.print('AUTOEARN数据库连接成功',style="#ADFF2F")
    task_c = task_conn.cursor()
    for url in url_result:
        now_time = time.strftime("%Y_%m_%d_%H_%M_%S", time.localtime())
        try:
            task_c.execute("INSERT INTO TASK (URL,TASK_TIME) VALUES ('%s', '%s')"%(url,now_time))
            task_conn.commit()
        except:
            console.print('插入任务数据库失败',style="bold red")
    console.print('插入任务数据库成功',style="#ADFF2F")
    task_conn.close()

注意这里面有点问题:问题在于:端口扫描没有执行,只是根据Shodan进行端口扫描,根本没有进行nmap扫描,主要是函数没调用,自己可以改下。改下: url_list.extend(shodan_port_check(check_cdn.check_cdn(domain[1])[0],domain[1]))这句即可。

4.3 WAF指纹识别

waf检测不检测都行,可选步骤,检测代码如下,调用wafw00f进行检测,更新下WAF检测到内容和STATUS为检测完成状态。

import json
import sqlite3
import subprocess
from lib import config
from rich.console import Console


console = Console()


# WAF检测函数
def waf_check(domain_list):
    console.print('正在进行WAF检测',style="#ADFF2F")
    console.print('任务数据库连接成功',style="#ADFF2F")
    conn = sqlite3.connect(config.result_sql_path)
    c = conn.cursor()
    for domain in domain_list:
        domain = domain[1]
        cmd = ['python3', config.wafw00f_path, domain]
        rsp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        for i in (rsp.stdout.read().decode("GBK").split('\n')):
            if 'url' in i:
                url = json.loads(i.replace('\'', '\"'))['url']
                waf = json.loads(i.replace('\'', '\"'))['waf'][0]
                c.execute("UPDATE TASK set WAF = '%s' where URL = '%s' "%(waf, url))
                c.execute("UPDATE TASK set STATUS = 'WAF检测完成' where URL = '%s' "%(url,))
                conn.commit()
        while True:
            if rsp.poll() == None:
                pass
            else:
                break
    console.print('WAF检测完成',style="#ADFF2F")
    conn.close()

4.4 漏洞检测

这步骤是关键步骤,通过爬取网页,然后调用xray进行相关漏洞扫描。调用代码:

craw_to_xray.craw_to_xray(sql_connect.read_task_sql())

爬虫代码也比较简单,主要是利用crawlergo进行网页的爬取。

crawlergo是一个使用chrome headless模式进行URL收集的浏览器爬虫。它对整个网页的关键位置与DOM渲染阶段进行HOOK,自动进行表单填充并提交,配合智能的JS事件触发,尽可能的收集网站暴露出的入口。内置URL去重模块,过滤掉了大量伪静态URL,对于大型网站仍保持较快的解析与抓取速度,最后得到高质量的请求结果集合。

import sqlite3
import subprocess
from lib import config
from rich.console import Console

console = Console()


# 爬虫爬取并且发送到XRAY
def craw_to_xray(domain_list):
    console.print('正在进行爬虫探测+漏洞检测',style="#ADFF2F")
    console.print('任务数据库连接成功',style="#ADFF2F")
    conn = sqlite3.connect(config.result_sql_path)
    c = conn.cursor()
    for domain in domain_list:
        domain = domain[1]
        # cmd = [config.crawlergo_path, "-c", config.chrome_path,"-t",config.max_tab_count, "-f", "smart", "--fuzz-path", "--push-to-proxy",config.push_to_proxy,  "--push-pool-max", config.max_send_count, domain]
        cmd = config.crawlergo_path + " -c " + config.chrome_path + " -t " + config.max_tab_count + " -f " + " smart " + " --fuzz-path " + " --push-to-proxy " + config.push_to_proxy + " --push-pool-max " + config.max_send_count + " " + domain 
        # rsp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        console.print('即将开启爬虫模块,可通过[bold cyan]tail -f logs/xray.log[/bold cyan]查看进度信息',style="#ADFF2F")
        rsp = subprocess.Popen(cmd, shell=True)
        while True:
            if rsp.poll() == None:
                pass
            else:
                break

采集完成后,通过--push-to-proxy推送给xray 开启的代理:# Xray被动代理地址 push_to_proxy = "http://127.0.0.1:7777"

这是start.sh 里面开启的:

nohup ./tools/xray/xray_linux_amd64 webscan --listen 127.0.0.1:7777 --webhook-output http://127.0.0.1:2333/webhook > logs/xray.log 2>&1 &

注意下webhook,这是xray的扫描结果推送给这个连接:http://127.0.0.1:2333/webhook 这个代码处理在server.py中如下:

@app.route('/webhook', methods=['POST'])
def xray_webhook():
    vuln = request.json
    # 因为还会收到 https://chaitin.github.io/xray/#/api/statistic 的数据
    if "vuln_class" not in vuln:
        return "ok"
    content = """```xray 发现了新漏洞```
### url: {url}
### 插件: {plugin}
### 漏洞类型: {vuln_class}
### 发现时间: {create_time}
```请及时查看和处理```
""".format(url=vuln["target"]["url"], plugin=vuln["plugin"],
           vuln_class=vuln["vuln_class"] or "Default",
           create_time=str(datetime.datetime.fromtimestamp(vuln["create_time"] / 1000)))
    try:
        push_ftqq(content)
        sql_connect.insert_vuln_sql(vuln)
    except Exception as e:
        logging.exception(e)
    return 'ok'

做了两件事情:1. 将漏洞推送给微信通知;2. 将漏洞信息保存到漏洞库做备份。

0x05 总结下

整个系统来说,功能完备,比较清晰,可以作为基础的框架继续做优化,想直接使用,还是欠缺的。 后续的改造点:

  1. target.txt 的域名通过采集获取,获取有赏金的域名。

  2. 域名每次只扫描新增加的域名,全量扫描太慢了。

  3. 端口探测,要增强下,把nmap和msscan调用起来,现在没有。

  4. 整个程序还处于半自动的,想完全执行起来,还需要建立个循环。

  5. 如果说框架有什么缺点,那就是插件之间数据传递不够标准,有的通过数据库,有的不是,建议改成成统一接口,这样好插拔控制插件,更灵活。

最后如果对这方面有兴趣,还是建议看看官方文档,讲的非常好:从零开始写自动化漏洞猎人

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/681947.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

你不得不知道的箭头函数和普通函数使用区别!

前言 箭头函数是 ES6 新增的一种函数类型,它采用箭头 > 定义函数,也称为 lambda 函数。箭头函数语法更为简洁,用起来很是方便顺手。 但它存在一些需要注意的问题和局限性。在实际使用时,我们需要根据具体情况来选择合适的函数…

【夜深人静学数据结构与算法 | 第九篇】栈与队列

目录 ​前言: 栈: 栈的实际应用: 队列: 队列的实际应用: 总结: 前言: 栈与队列是我们学习的两个经典的数据结构,这两个数据结构应用广泛,在计算机内有很多底层应用…

操作系统2——进程的描述与控制

本系列博客重点在深圳大学操作系统课程的核心内容梳理,参考书目《计算机操作系统》(有问题欢迎在评论区讨论指出,或直接私信联系我)。 梗概 本篇博客主要介绍操作系统第二章进程的描述与控制的相关知识。 目录 一、前驱图与程序…

大数据从0到1的完美落地之Flume案例1

案例演示 案例演示:AvroMemoryLogger Avro Source:监听一个指定的Avro端口,通过Avro端口可以获取到Avro client发送过来的文件,即只要应用程序通过Avro端口发送文件,source组件就可以获取到该文件中的内容,输出位置为…

数据库详细讲解--下

数据库详细讲解–下 mysql 表外连接 外连接 1.左外连接(如果左侧的表完全显示我们就说是左外连接) 2.右外连接(如果右侧的表完全显示我们就说是右外连接) 3.使用左连接(显示所有人的成绩,如果没有成绩…

4自由度并联机器狗实现行走功能

1. 功能说明 本文示例将实现R328a样机4自由度并联机器狗行走的功能。 2. 电子硬件 在这个示例中,我们采用了以下硬件,请大家参考: 主控板 Basra主控板(兼容Arduino Uno)‍ 扩展板 Bigfish2.1扩展板‍ 电池7.4V锂电池 …

【Linux】进程优先级 进程切换 环境变量

目录 一、进程优先级 1、优先级概念 2、优先级特点 3、修改Linux下的优先级 二、进程切换 1、进程特性 2、进程切换 三、环境变量 1、基本概念 2、常见环境变量 3、查看环境变量方法 4、PATH环境变量 5、和环境变量相关的命令 6、环境变量的组织方式 7、通过代码如何获取环境…

io.netty学习(十一)Reactor 模型

目录 前言 传统服务的设计模型 NIO 分发模型 Reactor 模型 1、Reactor 处理请求的流程 2、Reactor 三种角色 单Reactor 单线程模型 1、消息处理流程 2、缺点 单Reactor 多线程模型 1、消息处理流程 2、缺点 主从Reactor 多线程模型 主从Reactor 多线程模型示例 1…

索尼笔记本U盘重装Win10系统教程图解

很多使用索尼笔记本的用户想要给笔记本重装一下Win10系统,但不清楚具体要怎么操作,首先用户需要确保自己的索尼笔记本电脑能够正常联网,还需要准备一个8G以上的U盘,接着按照小编分享的索尼笔记本U盘重装Win10系统教程图解操作&…

怎么高效的通过爬虫获取数据

导语:在当今数字化时代中,获取数据已成为许多企业和个人的重要需求。在快速获取数据时,通过爬虫技术迅速获取网络数据已成为一项重要的技能和技术。然而,在应用爬虫技术前,需要注意一些重要的问题。本文总结了从数据来…

九大数据结构

数据结构想必大家都不会陌生,对于一个成熟的程序员而言,熟悉和掌握数据结构和算法也是基本功之一。数据结构本身其实不过是数据按照特点关系进行存储或者组织的集合,特殊的结构在不同的应用场景中往往会带来不一样的处理效率。 常用的数据结…

IDEA上面书写wordcount的Scala文件具体操作

系列文章目录 IDEA创建项目的操作步骤以及在虚拟机里面创建Scala的项目简单介绍_intellij 创建scala 目录 系列文章目录 1、编写Scala程序 2、更换pom.xml文件 3、更新Maven的依赖文件 4、执行代码即可 总结 前言 本文主要在上述文章的基础上编辑和创建一个WordCount应…

Linux常用命令——ftp命令

在线Linux命令查询工具 ftp 用来设置文件系统相关功能 补充说明 ftp命令用来设置文件系统相关功能。ftp服务器在网上较为常见,Linux ftp命令的功能是用命令的方式来控制在本地机和远程机之间传送文件,这里详细介绍Linux ftp命令的一些经常使用的命令…

【23-06-25:window基础命令学习】

目录 命令提示符cd /? 查看cd 语法 切换文件目录cd /d d:\ 改变当前的驱动器到D盘, 根目录切换到D盘,因为改变了驱动器,所以需要加上 /dDIRmd 命令 创建目录(文件夹) ,也可以直接创建多级子目录![在这里插…

MySql进阶篇(1)

MySql进阶篇 一、存储引擎1.1 MySql体系结构1.2 存储引擎的简介1.3 存储引擎的特点1.3.1 innoDB1.3.2 MyISAM1.3.3 Memory1.3.4 上述总结 1.4 存储引擎的选择 二、索引2.1 索引的概述2.2 索引结构2.2.1 二叉树和红黑树2.2.2 B-Tree(多路平衡查找树)2.2.3…

基于Java+Swing实现天气预报系统

基于JavaSwing实现天气预报系统 一、系统介绍二、功能展示1.主面2.IP定位城市3.通过城市名称查询天气状态4、查看各城区的天气 三、代码展示四、其他系统五、获取源码 一、系统介绍 系统主要通过输入城市名字(可通过电脑IP查询所属城市,查询所属城市IP接…

Linux系统编程(exec函数家族和system函数)

文章目录 前言一、exec函数家族二、system函数介绍三、system函数使用总结 前言 本篇文章我们继续讲解多进程编程中的,exec函数家族和system函数。 一、exec函数家族 exec 函数家族是一组在类Unix操作系统中常用的函数,用于在当前进程中执行一个新的程…

STM32模拟I2C协议获取HDC1080温度和湿度传感器数据

STM32模拟I2C协议获取HDC1080温度和湿度传感器数据 HDC1080是一款温湿度传感器,具有如下特点: 其中温度和湿度经过出厂校准。这里介绍STM32模拟I2C总线协议访问HDC1080的HAL库实现范例。 HDC1080电路连接 HDC1080的内部原理及电路连接如下&#xff1…

GIS 功能模块设计

文章目录 1 .地图渲染2. 地图控制设备管理模块设计1 . 导航树管理2. 查询定位功能3. 资源管理功能4 . 入沟管理管线业务功能模块设计1 .轨迹图管理2. 单线图管理3. 设备接线图管理4. 工井立视图管理其他管理模块1 .用户管理功能2. 数据导入功能 1 .地图渲染 主要解决将指定的空…

16个小的UI设计规则却能产生巨大的影响

微信搜索 【大迁世界】, 我会第一时间和你分享前端行业趋势,学习途径等等。 本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。 快来免费体验ChatGpt plus版本的,我们出的钱 体验地…