夕立
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

sonarqube介绍
在使用开源框架实现公司代码审计能力的过程中,比较了一些项目之后最后还是决定使用sonarqube,主要原因是图形化界面操作比较方便同时强大的语法解析能力也支持比较强的扩展性,以下是摘自官网的简单介绍:
SonarQube是一种自动代码审查工具,用于检测代码中的错误、漏洞和代码异味。它可以与您现有的工作流程(例如 Jenkins)集成,以实现跨项目分支和拉取请求的持续代码检查。
centos安装和配置
为sonar创建用户
创建一个单独的用户来运行 SonarQube命令如下:
sudo useradd -M -d /opt/sonarqube/ -r -s /bin/bash sonar
由于sonar会用到自带的es,所以不能用root用户,否则后续启动服务的时候会有类似的报错:
安装java环境
SonarQube 是用 Java 编写,而且不同版本的sonarqube支持的java环境不同,具体可以参照文档:https://docs.sonarqube.org/latest/requirements/requirements/
使用 sonarqube 对 java 项目代码进行扫描的时候,java 项目的版本不能低于 sonar 的编译版本。
我们以最新版本sonarqube为例(8.9),安装java17
sudo yum -y install java-17-openjdk-devel
通过检查版本确认 Java 安装成功。
$ java -version
openjdk version "17.0.7" 2023-04-18 LTS
OpenJDK Runtime Environment (Red_Hat-17.0.7.0.7-1.el8_7) (build 17.0.7+7-LTS)
OpenJDK 64-Bit Server VM (Red_Hat-17.0.7.0.7-1.el8_7) (build 17.0.7+7-LTS, mixed mode, sharing)
安装并配置 PostgreSQL
SonarQube需要依赖数据库存储数据,且SonarQube7.9及其以后版本将不再支持Mysql,所以这里推荐设置PostgreSQL作为SonarQube的数据库。我们将在 SonarQube 所在的同一服务器上安装 PostgreSQL 13 服务器。也可以根据需要将其托管在不同的服务器中。
PostgreSQL安装比较简单可以参考官网:https://www.postgresql.org/download/linux/redhat/
安装完成后,我们可以成功连接到 Postgres 数据库,我们可以继续为 SonarQube 创建用户和数据库。
创建SonarQube用户和数据库
在这里,我们将为 SonarQube 创建一个用户。在退出数据库之前,请按如下所示继续操作。
sudo su - postgres
psql
create user sonar;
create database sonar_db owner sonar;
grant all privileges on database sonar_db to sonar;
为创建的用户设置密码
ALTER USER sonar WITH ENCRYPTED password 'StrongPassword';
\q
exit
安装 SonarQube
可以访问 SonarQube 下载页面来查看他们的各种产品。这里以下载开发者版本为例:
cd ~/
wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-9.9.1.69595.zip
然后解压文件
sudo yum -y install unzip
unzip sonarqube-*.zip
之后,将文件夹重命名为 sonarqube
sudo mv sonarqube-*/ /opt/sonarqube
rm sonarqube-*.zip
配置 SonarQube
将文件提取到/opt/目录后,就可以配置应用程序了。
打开“/opt/sonarqube/conf/sonar.properties”文件并添加数据库详细信息,如下所示。除此之外,找到共享的行并取消注释它们。
$ sudo vim /opt/sonarqube/conf/sonar.properties
##Database details
sonar.jdbc.username=sonar
sonar.jdbc.password=StrongPassword
sonar.jdbc.url=jdbc:postgresql://localhost/sonar_db
##How you will access SonarQube Web UI
sonar.web.host=0.0.0.0
sonar.web.port=9000
##Java options
sonar.web.javaOpts=-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError
sonar.search.javaOpts=-Xmx512m -Xms512m -XX:MaxDirectMemorySize=256m -XX:+HeapDumpOnOutOfMemoryError
##Also uncomment the following Elasticsearch storage paths
sonar.path.data=data
sonar.path.temp=temp
为方便管理soanr的用户,我们也可以将配置文件中的usermap与ldap服务器进行关联
# USER MAPPING
# Distinguished Name (DN) of the root node in LDAP from which to search for users (mandatory)
ldap.user.baseDn=OU=
# LDAP user request. (default: (&(objectClass=inetOrgPerson)(uid={login})) )
ldap.user.request=(&(objectClass=user)(sAMAccountName={login}))
# Attribute in LDAP defining the user’s real name.
ldap.user.realNameAttribute=
# Attribute in LDAP defining the user’s email.
ldap.user.emailAttribute=
将 SonarQube 文件所有权授予我们在步骤 1 中创建的声纳用户。
sudo chown -R sonar:sonar /opt/sonarqube
如果在默认位置找不到 Java,必须指定 SonarQube 才能查找的二进制文件。可以指定 java 在“/opt/sonarqube/conf/wrapper.conf”文件中的位置。查找“wrapper.java.command”行并将 Java 位置放在它旁边。
$ sudo vim /opt/sonarqube/conf/wrapper.conf
wrapper.java.command=/usr/lib/jvm/jre-openjdk/bin/java
添加SonarQube SystemD服务文件
最后,为确保能够通过 Systemd 管理 SonarQube 应用程序,以便可以像服务器中的其他服务一样启动和停止它
$ sudo vim /etc/systemd/system/sonarqube.service
[Unit]
Description=SonarQube service
After=syslog.target network.target
[Service]
Type=forking
ExecStart=/opt/sonarqube/bin/linux-x86-64/sonar.sh start
ExecStop=/opt/sonarqube/bin/linux-x86-64/sonar.sh stop
LimitNOFILE=65536
LimitNPROC=4096
User=sonar
Group=sonar
Restart=on-failure
[Install]
WantedBy=multi-user.target
编辑systemd文件后,我们必须重新加载它们,以便可以读取和加载它们。
sudo systemctl daemon-reload
然后启动并启用服务
sudo systemctl start sonarqube.service
sudo systemctl enable sonarqube.service
检查其状态是否已成功启动并正在运行。
$ systemctl status sonarqube.service
● sonarqube.service - SonarQube service
Loaded: loaded (/etc/systemd/system/sonarqube.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2021-07-27 18:49:18 EAT; 6s ago
Process: 43024 ExecStart=/opt/sonarqube/bin/linux-x86-64/sonar.sh start (code=exited, status=0/SUCCESS)
Main PID: 43073 (wrapper)
Tasks: 36 (limit: 4522)
Memory: 548.3M
CGroup: /system.slice/sonarqube.service
├─43073 /opt/sonarqube/bin/linux-x86-64/./wrapper /opt/sonarqube/bin/linux-x86-64/../../conf/wrapper.conf wrapper.sy>
更改防火墙规则以允许 SonarQube 访问
此时,sonarqube 服务应该正在运行。如果无法访问 Web 界面,可以访问位于“/opt/sonarqube/logs”中的日志文件,您将在其中找到:
- 弹性搜索日志(es.log)
- 声纳日志 (sonar.log)
- 网络日志 (web.log)
- 访问日志(access.log)
- 其它日志
由于前面启用 SonarQube Web 来侦听端口 9000。为了使一切正常工作,我们应该在防火墙上允许此端口。通过运行下面共享的命令继续执行此操作。
sudo firewall-cmd --permanent --add-port=9000/tcp && sudo firewall-cmd --reload
访问 Web 用户界面
使用浏览器访问http://server-ip-or-fqdn:9000。默认的账号密码是admin/admin
之后,系统会要求将密码更新为新密码。输入新的管理员密码并继续登录。
K8S安装sonarqube
手动安装sonarqube比较繁琐,现在一般使用docker的方式,这边也是用k8s部署了一遍,确实只需要改一改配置文件就可以启动,相当方便,yaml文件如下:
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"labels":{"app":"postgres-sonar"},"name":"postgres-sonar","namespace":"sonarqube"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"postgres-sonar"}},"template":{"metadata":{"labels":{"app":"postgres-sonar"}},"spec":{"containers":[{"env":[{"name":"POSTGRES_DB","value":"sonarDB"},{"name":"POSTGRES_USER","value":"sonarUser"},{"name":"POSTGRES_PASSWORD","value":"strongpassword"}],"image":"postgres:11.4","imagePullPolicy":"IfNotPresent","name":"postgres-sonar","ports":[{"containerPort":5432}],"resources":{"limits":{"cpu":"1000m","memory":"2048Mi"},"requests":{"cpu":"128m","memory":"512Mi"}},"volumeMounts":[{"mountPath":"/var/lib/postgresql/data","name":"data"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"postgres-data"}}]}}}}
creationTimestamp: "2023-10-07T07:38:28Z"
generation: 1
labels:
app: postgres-sonar
managedFields:
- apiVersion: apps/v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.: {}
f:kubectl.kubernetes.io/last-applied-configuration: {}
f:labels:
.: {}
f:app: {}
f:spec:
f:progressDeadlineSeconds: {}
f:replicas: {}
f:revisionHistoryLimit: {}
f:selector:
f:matchLabels:
.: {}
f:app: {}
f:strategy:
f:rollingUpdate:
.: {}
f:maxSurge: {}
f:maxUnavailable: {}
f:type: {}
f:template:
f:metadata:
f:labels:
.: {}
f:app: {}
f:spec:
f:containers:
k:{"name":"postgres-sonar"}:
.: {}
f:env:
.: {}
k:{"name":"POSTGRES_DB"}:
.: {}
f:name: {}
f:value: {}
k:{"name":"POSTGRES_PASSWORD"}:
.: {}
f:name: {}
f:value: {}
k:{"name":"POSTGRES_USER"}:
.: {}
f:name: {}
f:value: {}
f:image: {}
f:imagePullPolicy: {}
f:name: {}
f:ports:
.: {}
k:{"containerPort":5432,"protocol":"TCP"}:
.: {}
f:containerPort: {}
f:protocol: {}
f:resources:
.: {}
f:limits:
.: {}
f:cpu: {}
f:memory: {}
f:requests:
.: {}
f:cpu: {}
f:memory: {}
f:terminationMessagePath: {}
f:terminationMessagePolicy: {}
f:volumeMounts:
.: {}
k:{"mountPath":"/var/lib/postgresql/data"}:
.: {}
f:mountPath: {}
f:name: {}
f:dnsPolicy: {}
f:restartPolicy: {}
f:schedulerName: {}
f:securityContext: {}
f:terminationGracePeriodSeconds: {}
f:volumes:
.: {}
k:{"name":"data"}:
.: {}
f:name: {}
f:persistentVolumeClaim:
.: {}
f:claimName: {}
manager: kubectl-client-side-apply
operation: Update
time: "2023-10-07T07:38:28Z"
- apiVersion: apps/v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
f:deployment.kubernetes.io/revision: {}
f:status:
f:availableReplicas: {}
f:conditions:
.: {}
k:{"type":"Available"}:
.: {}
f:lastTransitionTime: {}
f:lastUpdateTime: {}
f:message: {}
f:reason: {}
f:status: {}
f:type: {}
k:{"type":"Progressing"}:
.: {}
f:lastTransitionTime: {}
f:lastUpdateTime: {}
f:message: {}
f:reason: {}
f:status: {}
f:type: {}
f:observedGeneration: {}
f:readyReplicas: {}
f:replicas: {}
f:updatedReplicas: {}
manager: kube-controller-manager
operation: Update
time: "2023-10-07T07:40:27Z"
name: postgres-sonar
namespace: sonarqube
resourceVersion: "62739537"
selfLink: /apis/apps/v1/namespaces/sonarqube/deployments/postgres-sonar
uid: 69b0e8e4-ea9a-4e2f-aa93-8aa8324d1d07
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: postgres-sonar
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: postgres-sonar
spec:
containers:
- env:
- name: POSTGRES_DB
value: sonarDB
- name: POSTGRES_USER
value: sonarUser
- name: POSTGRES_PASSWORD
value: strongpassword
image: postgres:11.4
imagePullPolicy: IfNotPresent
name: postgres-sonar
ports:
- containerPort: 5432
protocol: TCP
resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: 128m
memory: 512Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: data
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
status:
availableReplicas: 1
conditions:
- lastTransitionTime: "2023-10-07T07:40:27Z"
lastUpdateTime: "2023-10-07T07:40:27Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
- lastTransitionTime: "2023-10-07T07:38:28Z"
lastUpdateTime: "2023-10-07T07:40:27Z"
message: ReplicaSet "postgres-sonar-7968cc9c4f" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
observedGeneration: 1
readyReplicas: 1
replicas: 1
updatedReplicas: 1
部署完成后 通过nodeport方式访问即可
漏洞扫描规则
作为安全工程师,相对比较关注的还是sonarqube能支持的漏洞规则,主要分为两项:
漏洞 和 安全热点,不同语言的支持项不同,数据比较少,这个地方我们需要导入自定义规则或者使用现成的插件。
导入插件
第一种方式比较简单,在web页面直接安装
但是由于这种方式是部署sonarqube的服务器直接连接插件下载链接(通常是github)可能存在网络不通或者连接不稳定的情况。所以这边我们也是使用第二种方式安装插件,方法也相当简单:
在应用市场界面点击插件的首页,下载release的jar包
放到${SONAR_HOME}/extensions/plugins目录下,然后重启SonarQube
这边比较推荐安装的插件是:
Chinese Pack 中文安装包
Findbugs 增加安全扫描的漏洞规则库
Community Branch Plugin 管理分支信息
自定义规则编写
自定义规则编写也是提高代码审计效率和准确性的重要环节,此处以大部分公司使用的java为例:
下载sonar-java插件源代码,这也是Java扫描规则集,我们会基于这个规则集编写我们自己的规则,下载地址https://github.com/SonarSource/sonar-java
sonarqube的java自定义规则构成主要有以下部分:
- java-check中添加一条规则
- java-check test模块中添加测试用例
- java-check resource模块中添加规则描述,包括一个html和一个json文件
- 在org.sonar.java.checks.CheckList中注册规则
直接上一个Struts2 S2-057检查规则的例子
说明:扫描项目pom.xml中是否使用包含S2-057漏洞版本的struts2依赖
package org.sonar.java.checks.xml.maven;
import org.sonar.java.checks.xml.maven.helpers.MavenDependencyCollector;
import org.sonar.java.xml.maven.PomCheck;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.java.xml.maven.PomCheckContext;
import org.sonar.maven.model.LocatedAttribute;
import org.sonar.maven.model.maven2.Dependency;
import javax.annotation.Nullable;
import java.util.List;
@Rule(key = "Struts2_S2_057Check")
public class Struts2_S2_057Check implements PomCheck {
@Override
public void scanFile(PomCheckContext context) {
List<Dependency> dependencies = new MavenDependencyCollector(context.getMavenProject()).allDependencies();
for (Dependency dependency : dependencies) {
LocatedAttribute artifactId = dependency.getArtifactId();
LocatedAttribute version = dependency.getVersion();
if (version != null && artifactId != null && "struts2-core".equalsIgnoreCase(artifactId.getValue()) && !strutsVerCompare(version.getValue())) {
String message = "此版本Struts2包含高危漏洞";
List<PomCheckContext.Location> secondaries = getSecondary(version);
int line = version.startLocation().line();
context.reportIssue(this, line, message, secondaries);
}
}
}
private static List<PomCheckContext.Location> getSecondary(@Nullable LocatedAttribute systemPath) {
if (systemPath != null && StringUtils.isNotBlank(systemPath.getValue())) {
return Lists.newArrayList(new PomCheckContext.Location("configure check", systemPath));
}
return ImmutableList.of();
}
private static boolean strutsVerCompare(String version){
String StrutsVersion1 = "2.3.35";
String StrutsVersion2 = "2.5.17";
String[] versionArray1 = version.split("\\.");
if(versionArray1[1].equalsIgnoreCase("3")){
if(compareVersion(StrutsVersion1, version) > 0){
return false;
}
}
if(versionArray1[1].equalsIgnoreCase("5")){
if(compareVersion(StrutsVersion2, version) > 0){
return false;
}
}
return true;
}
private static int compareVersion(String version1, String version2){
String[] versionArray1 = version1.split("\\.");
String[] versionArray2 = version2.split("\\.");
int idx = 0;
int minLength = Math.min(versionArray1.length, versionArray2.length);
int diff = 0;
while (idx < minLength
&& (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
&& (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
++idx;
}
diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
return diff;
}
}
几个关键点如下:
- @Rule(key = "Struts2_S2_057Check")该注解声明本条规则的Key
- implements PomCheck
public void scanFile(PomCheckContext context)
本条规则实现PomCheck类,重写scanFile,这样插件和扫描器会自动解析Pom.xml文件,并将解析完后的pom文件语法树传递进来 - strutsVerCompare 用来定义哪些版本的Struts2依赖存在漏洞
之后在sonar-java/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/中新建Struts2_S2_057Check_java.json和Struts2_S2_057Check_java.html两个文件,可以参考其他规则来编写。最后在CheckList中进行注册
集成SDL环境
部署并配置好sonarqube的规则之后,就需要将代码审计集成到sdl的流程中去,一般可以参考公司不同的流程对接sonarqube部署机器,最简单的方式就是在每次发布前将打包好的代码传给sonarqube,在机器上进行sonar-runner,就可以完成一次扫描,然后根据返回结果来进行卡点,还有一种方式也是我们目前在使用的,就是在idea里集成,用maven的方式上传代码进行扫描。当然sonarqube本身也提供了一些和开发环境集成的功能。
与gitlab集成
GitLab配置个人访问令牌
至少拥有reporter权限的账号登录GitLab
创建个人访问令牌,注意:不要使用管理员账号,管理员账号没有项目权限;需要账号在项目下;
管理员登录sonarqube,创建配置
在弹出框中,输入上步生成的个人访问令牌,GitLab地址为gitlab的api地址,可以访问/help/api/README.md进行查看
新增项目–>gitlab,输入访问令牌,就可以用gitlab导入代码扫描项目了
与jenkins集成
在Jenkins>Manage jenkins>Manage Plugins中搜索sonarqube,安装SonarQube Scanner for Jenkins插件;
重启Jenkins后,在Manage jenkins>Global Tool Configuration中配置Sonarquebe Scanner
在sonarqube页面中,点击用户头像,点击我的账号;在新页面中,点击“安全”table,输入令牌名称,点击生成,生成令牌文件;此令牌文件用于Jenkins和sonarqube通信的安全验证,且此令牌内容只显示一次,及时保存在其他文本中,避免遗失;
在Jenkins中配置sonarqube server
Manage jenkins>Configure system,新增sonarqube server服务;
其中,token需要使用secret text类型的凭据,将sonarqube生成的令牌粘贴到Secret中;
配置完之后使用过程如下:
创建一个freestyle的项目,名称自定义,在项目配置中进行如下配置
1,分支选择配置,如下图
如果没有此选项,需要安装Git Parameter插件;
2,配置git
3,配置build
#sonar工程标识,随意输入不重复有代表意义即可
sonar.projectKey=bi_build_sonar
#sonar工程标识,随意输入不重复有代表意义即可
sonar.projectName=bi_build_sonar
#sonar工程版本号
sonar.projectVersion=1.0
#源代码路径,依据需要可在$WORKSPACE后加入目标路径,缩小分析范围;
sonar.sources=$WORKSPACE
#class文件路径,依据需要可在$WORKSPACE后加入目标路径,缩小分析范围;
sonar.java.binaries=$WORKSPACE
4,执行
点击build with parameters,选择分支,点击build;
分析完成,点击SonarQube,即可进入sonarqube服务中,查看代码分析的结果;
踩坑记录
1.启动sonar
如果没有注册服务,systemctl start sonarqube.service 是没法启动sonarqube的,笔者也是接手了一个之前的sonarcube发现之前部署的时候没有做服务注册和自启动的设置,所以也可以直接在linux的sonar安装bin目录如.\sonarqube-4.5.7\bin\linux-x86-64,运行如下命令
./sonar.sh restart 重启服务:
./sonar.sh stop停止服务
./sonar.sh start启动服务
需要注意的是重启的时候切到非root用户,否则会起不来,原因就是上文所说的es没法用root启动,可以查看一下文件权限给到了哪个用户。如果遇到es无法以root身份启动的问题,也可以直接无脑执行以下步骤:
- 添加用户sudo useradd 声纳
- 添加群组sudo groupadd 声纳
- 授予声纳许可sudo chown -R 声纳:声纳 {声纳安装文件夹}/
- 将 sonar.properties 文件编辑为用户“sonar”用户现在编辑位于/sonar/conf下的 sonar.sh 文件 并将 #RUN_AS_USER 更改为 RUN_AS_USER=sonar
- 运行声纳转到安装声纳的文件夹(在我的例子中为/opt/sonar)/sonar/bin/linux-x86-64$ sudo ./sonar.sh 启动
- 检查状态/sonar/bin/linux-x86-64$ ./sonar.sh 状态
2.自签名证书
在使用sonar连接公司内部自签名证书的gitlab时遇到了报错,参考官方提供的解决方案:
需要将 CA 添加到 SonarQube 的 java 信任库中,
传统的压缩包安装方式中,在系统信任库中
找到 $JAVA_HOME/lib/security/cacerts。将新证书添加到信任库,参考以下命令作为示例:
keytool -importcert -file $PATH_TO_CERTIFICATE -alias $CERTIFICATE_NAME -keystore /$JAVA_HOME/lib/security/cacerts -storepass changeit -trustcacerts -noprompt
使用docker 镜像方式安装的情况下,可以找到系统信任库 <JAVA_HOME>/lib/security/cacerts。
- 将包含证书的现有信任库绑定挂载到 <JAVA_HOME>/lib/security/cacerts.
例子
docker run -d --name sonarqube -v /path/to/your/cacerts.truststore:/opt/java/openjdk/lib/security/cacerts:ro -p 9000:9000 sonarqube
- 以与 压缩包 安装相同的方式导入 CA 证书,但在容器内。
如果使用官方 Helm Chart 在 Kubernetes 上部署 SonarQube,可以创建一个包含所需证书的新密钥,并通过以下方式引用:
caCerts:
enabled: true
image: adoptopenjdk/openjdk17:alpine
secret: your-secret
3.权限管控
建议对接Gitlab或者ldap等服务来避免繁琐的用户/权限管控,同时把所有新项目设为私有可以有效解决信息泄露的问题。当一个新的用户需要看到项目内容时,在项目中增加用户即可
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee2022)