本来这个是打算在 Insomni’hack 2025 讲的, 由于个人原因没办法参与, 所以写个blog分享一下. 本文主要内容包括我是如何开始的Azure bounty program的研究, 如何扩展的研究, 以及部分成果的分享.
致谢 我在 Azure 下的所有成果都离不开肖伟 大神的帮助, 感谢他的指导和分享.
我也非常感谢yuki chen 的指导和分享, 这个故事里似乎没有他, 但是其实是他带我开始了azure之路. 而且他给我介绍了一个价值连城的目标(sqlcmd), 还无私分享了他的发现, 可是我自己太菜, 没有找出更多的问题, 错失了更进一步的机会. 而他后续又在这个目标发现了非常多的问题, 又一次让人望尘莫及地成为了top1, Orz.
我还要感谢 bee13oy , 他给我分享了sqlcmd的发现, 让我可以提高自身的水平, 弥补自己的大意. 如果我可以更仔细分析里面的每一个案例, 或许就不会错过这个目标了.
Azure Bounty Program 规则 知己知彼, 百战不殆. 要想挖微软的azure bounty program , 先看看他们规则咋写的(2022年时):
可以看到, 它规定了目标得是Azure Products页面里的产品.
不属于范围内的规则(2022年时):
因为我主要擅长二进制相关的安全研究, 所以我比较关注的是二进制相关的产品. 这里列举的 Azure Site Recovery和Azure Defender for IOT 就是二进制程序. 所以, 这个奖励计划其实是包括二进制程序的. (以前我并没有意识到这个事情, 当我知道的时候, 那两个目标已经属于Out of Scope了 (T-T), 错过了一波致富经. 好在机会还是有的, 这次我没错过:)
Azure 产品 访问上面bounty页面的azure产品链接 , 就可以看到如下列表:
涵盖的产品非常多, 我只截取了一小部分. 而故事一开始的重点, 就在于Azure RTOS.
一开始, 是肖伟大神先看到了这个IOT系统, 鉴于微软的尿性, MSRC往往可能不认可该产品的漏洞属于Azure Bounty Program. 所以, 在提交之前, 我发了一封邮件给 bounty@microsoft.com , 问他们Azure RTOS的NextX和NetX Duo是否在奖励计划内, 好在这次他们答复了我:
然而, 有时候他们根本不会答复, 所以这次也很幸运.
一血 ICMP 在这一周内, 肖伟大神已经发现了好几个RCE的问题, 而我也开始了尝试, 希望可以在肖伟大神的后面拣点漏.
然而这块的代码还是比我想象中严谨, 我一开始并没有任何收获, 只有挫败感. 在肖伟大神分享了一两个发现后, 我才发现原来自己错过了那么多. 这也让我重新找回信心, 开始继续挖掘.
很快, 我在NetX发现了和肖伟大神在NetX Duo上发现的相似的问题, NetX Duo比NetX功能强大一点, 所以理论上NetX Duo应该包含NetX的代码, 没想到NetX还多了点bug.
一开始, 微软复现后, 就将NetX项目从Github移除了, 然后告诉我, 该项目已经被废弃, 不算在奖金范畴了, 于是我祭出当初的回复邮件, 以及他们给我的答复中关于项目何时被废弃的时间(即废弃时间在我提交bug的时间之后), 他们终于承认了这个漏洞并给予了奖励.
下面看看怎么回事:
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 ULONG _nx_icmp_checksum_compute(NX_PACKET *packet_ptr) { ULONG checksum = 0 ; ULONG long_temp; USHORT short_temp; ULONG length; UCHAR *word_ptr; NX_PACKET *current_packet; length = packet_ptr -> nx_packet_length; if (((length / sizeof (USHORT)) * sizeof (USHORT)) != length) { length++; if (packet_ptr -> nx_packet_last) { *((packet_ptr -> nx_packet_last) -> nx_packet_append_ptr) = 0 ; } else { *(packet_ptr -> nx_packet_append_ptr) = 0 ; } }
tag1处会向packet的nx_packet_append_ptr
指向处写入0. 然而, 如果packet是ip 分片传入的, 它可以在nx_packet_append_ptr
指向buffer末尾时, buffer长度还是奇数, 所以tag2的判断就成立, 导致越界写入了packet结构体的末尾. 而该结构体的末尾就是一个结构体指针, 所以刚好可能造成RCE.
二血 SNMP奖金池 在NetX Duo 的addon目录, 有很多网络服务:
其中, snmp, 就是此章节的重点, 我在其中总共发现了 12个漏洞, 虽然被合并了几个, 但是也足够幸运了.
简单列举一个案例:
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 buffer_length = (INT)(packet_ptr -> nx_packet_length); /* Setup a pointer to the buffer. */ buffer_ptr = packet_ptr -> nx_packet_prepend_ptr; ...... do { variable_start_ptr = buffer_ptr; length = _nx_snmp_utility_sequence_get(buffer_ptr, &variable_length, buffer_length); total_variable_length = variable_length + length;// tag1 length 为 len1 ...... buffer_ptr = buffer_ptr + length; buffer_length -= (INT)length; length = _nx_snmp_utility_object_id_get(buffer_ptr, agent_ptr -> nx_snmp_agent_current_octet_string, buffer_length); ...... buffer_ptr = buffer_ptr + length; buffer_length -= (INT)length;// tag2 length 为 len2 ...... if (length != variable_length) { length = _nx_snmp_utility_object_data_get(buffer_ptr, &(agent_ptr -> nx_snmp_agent_current_object_data), buffer_length); ...... } ...... buffer_ptr = variable_start_ptr + total_variable_length; variable_list_length = variable_list_length - total_variable_length; objects++; } while (variable_list_length);
在一次循环里, buffer_ptr
增加了 total_variable_length
的长度, 即 variable_length + len1, buffer_length
减少了len1+len2
. 而事实上, len2不一定等于variable_length. 当 variable_length > len2
时, 就会导致 buffer_ptr
的剩余空间小于buffer_length
的值. 而在_nx_snmp_utility_object_id_get
中, 会修改buffer的内容, 导致越界写入, 从而造成RCE.
snmp中其它的bug也差不多是这种越界写入的问题.
三血 FTP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 VOID _nx_ftp_server_command_process(NX_FTP_SERVER *ftp_server_ptr) { ...... client_req_ptr = &(ftp_server_ptr -> nx_ftp_server_client_list[i]); ...... switch(ftp_command) { ...... case NX_FTP_QUIT: { if (client_req_ptr -> nx_ftp_client_request_packet) { /* Yes, release it! */ nx_packet_release(client_req_ptr -> nx_ftp_client_request_packet); } ...... break; ...... case NX_FTP_RNFR: { ...... client_req_ptr -> nx_ftp_client_request_packet = packet_ptr;
这个漏洞比较简单, 就是这个函数可以重复多次, 而释放操作并没有置零client_req_ptr -> nx_ftp_client_request_packet
指针, 导致double free.
这个漏洞并不复杂, 我之所以要讲, 是因为我是通过vs code, 全局搜索所有的释放操作, 然后一个个检查是否有置零操作, 从而发现了它. 而我在AMQP项目也用同样的方法找到了好几个问题.
四血 double free 找完以上bug后, 似乎已经没什么新的问题了. 但是snmp中的一个问题, 引起了我的注意.
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 VOID _nx_snmp_version_1_process(NX_SNMP_AGENT *agent_ptr, NX_PACKET *packet_ptr) { do { variable_start_ptr = buffer_ptr; ...... if (length != variable_length) { length = _nx_snmp_utility_object_data_get(buffer_ptr, &(agent_ptr -> nx_snmp_agent_current_object_data), buffer_length); if (length != variable_length) { /* Pickup the value associated with this variable. */ length = _nx_snmp_utility_object_data_get(buffer_ptr, &(agent_ptr -> nx_snmp_agent_current_object_data), buffer_length); /* Determine if the object value was successful. */ if (length == 0) { /* Increment the invalid packet error counter. */ agent_ptr -> nx_snmp_agent_invalid_packets++; /* Increment the internal error counter. */ agent_ptr -> nx_snmp_agent_internal_errors++; /* Send an SNMP version error response. */ _nx_snmp_version_error_response(agent_ptr, packet_ptr, request_type_ptr, error_ptr, NX_SNMP_ERROR_BADVALUE, objects+1); // 1. 函数有可能释放 packet_ptr /* Release the packet. */ nx_packet_release(packet_ptr);// 2. 直接释放 packet_ptr } ...... } while (variable_list_length); } VOID _nx_snmp_version_error_response(NX_SNMP_AGENT *agent_ptr, NX_PACKET *packet_ptr, UCHAR *request_type_ptr, UCHAR *error_string_ptr, UINT error_code, UINT error_index) { ...... status = nxd_udp_socket_send(&(agent_ptr -> nx_snmp_agent_socket), packet_ptr, &(agent_ptr -> nx_snmp_agent_current_manager_ip), agent_ptr -> nx_snmp_agent_current_manager_port); if (status) { nx_packet_release(packet_ptr); } return; }
从上述操作可以明显看到, 假如nxd_udp_socket_send
返回非0结果, 它就会释放packet_ptr
, 而上层函数不管发生了什么, 都会再次释放packet_ptr.
如果是单线程, 这其实是没有问题的, 因为nx_packet_release
里有判断操作, 所以不会导致double free问题. 一开始我也是这么认为的. 直到某次测试时, 发现RTOS是多线程的. 然后重新重视起这个问题, 测试后发现, 确实可以多线程竞争, 造成double free的问题.
另外, 即使nxd_udp_socket_send
发送成功, 也会在nxd_udp_socket_send
内部的子函数实现中释放packet_ptr.
从这一个问题, 我就开始想, 是不是调用方并不知道nxd_udp_socket_send
其实会释放packet
?
通过遍历所有类似的调用函数, 我在不同模块里找到了7个相似问题. 还有几个还没有来得及确认, MSRC就已经将 RTOS加入out of scope了:
August 16, 2023: Added to out of scope – vulnerabilities found in Azure RTOS.
有好的目标确实应该不舍昼夜地挖, 不然你永远不知道他们什么时候不给钱了.
寻找下一个目标 在RTOS不给钱以后, 我只能再找找Azure的其它产品是否存在安全问题. 先后查看了以下产品:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Azure Data Factory Azure Stack Development Kit Azure Stack Hub Azure Communication Services Azure migrate Azure Storage Explorer Azure lustre Spatial Anchors/Remote Rendering Azure Object Anchors Azure Database Migration Service Azure Monitor Azure Update Manager microsoft purview Azure Arc Service Fabric
上述产品并没有深入, 我不擅长, 所以就没有深入. 中间提交了azure iot-plug-and-play-bridge, Azure Kinect SDK Depth Engine 的漏洞, 最后不在奖励范围内. 于是我继续寻找新目标.
在一个个翻找azure的产品及说明文档 , 寻找目标时, Github上的开源组件azure-uamqp-c 吸引了我的注意力. 它是一个消息传输协议, 并且在多个服务中看到它的存在. 而且还开源. 所以我开始寻找它上面的问题.
一鱼三吃 在花了几天时间分析amqp后, 我找到了一个越界写入问题.
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 case 0xB0 :{ if (internal_decoder_data->bytes_decoded < 4 ) { internal_decoder_data->decode_to_value->value.binary_value.length += buffer[0 ] << ((3 - internal_decoder_data->bytes_decoded) * 8 ); internal_decoder_data->bytes_decoded++; buffer++; size--; if (internal_decoder_data->bytes_decoded == 4 ) { if (internal_decoder_data->decode_to_value->value.binary_value.length == 0 ) { internal_decoder_data->decode_to_value->value.binary_value.bytes = NULL ; internal_decoder_data->on_value_decoded(internal_decoder_data->on_value_decoded_context, internal_decoder_data->decode_to_value); result = 0 ; } else { internal_decoder_data->decode_to_value->value.binary_value.bytes = (unsigned char *)malloc ((size_t )internal_decoder_data->decode_to_value->value.binary_value.length + 1 ); if (internal_decoder_data->decode_to_value->value.binary_value.bytes == NULL ) { ...... } else { result = 0 ; } } } else { result = 0 ; } } else { size_t to_copy = internal_decoder_data->decode_to_value->value.binary_value.length - (internal_decoder_data->bytes_decoded - 4 ); if (to_copy > size) { to_copy = size; } (void )memcpy ((unsigned char *)(internal_decoder_data->decode_to_value->value.binary_value.bytes) + (internal_decoder_data->bytes_decoded - 4 ), buffer, to_copy); buffer += to_copy; size -= to_copy; internal_decoder_data->bytes_decoded += to_copy; if (internal_decoder_data->bytes_decoded == (size_t )internal_decoder_data->decode_to_value->value.binary_value.length + 4 ) { internal_decoder_data->decoder_state = DECODER_STATE_CONSTRUCTOR; internal_decoder_data->on_value_decoded(internal_decoder_data->on_value_decoded_context, internal_decoder_data->decode_to_value); } result = 0 ; } break ; }
这个比较简单, 就是在代码注释位置, 32bit的程序存在整数溢出, 导致申请的内存size为0, 后续就溢出写入了.
我将该问题提交微软以后, 微软认可了这个目标, 我在等待官方出了补丁后, 又提交了相同的问题, 在相同的函数内, 不同的分支位置. 一般来说, 相同函数内出现的不同问题, 微软往往会合并他们, 所以我也没必要提交另一个分支的漏洞(RTOS 就被合并了). 然而这次, 修复者偷懒, 没有检查其它位置是否存在相同问题, 草草地修复了漏洞, 于是我可以在补丁出了后, 重新提交遗漏的位置. 这是一鱼两吃.
显然, 故事还没有结束. 我在搜索amqp函数的过程中, 偶然发现, 有其它的项目(Azure-sdk-for-cpp, azure-uamqp-python), 也包含了azure-uamqp-c的代码, 但是上次我提交的漏洞并没有被同步到这两个项目中. 于是我又用那个项目提交了相同的bug. 最后微软也认了, 修补了两个项目中的bug. 至此, 一鱼三吃的故事落下帷幕.
double free again 在snmp的double free启发我以后, 我在amqp, azure-c-shared-utility, azure-iot-sdk-c, azure iot device update 项目中应用以下规则, 我得以用最简单的方法找出了11个uaf的问题.
1 2 3 4 5 `(delete|destroy|free|release)\w*\([\w\(\*]`, *.h,*.c,*.cpp. 排除samples, tests 筛选原则: 1. 释放后未置零, 本地变量判断是否在当前函数循环内, 传入参数判断上层函数是否在循环内 2. 释放的变量是否存储到其它结构体里 (这个可能存在漏网之鱼) 3. 同一个函数存在多次释放(比如失败后释放了一次)
在2023年末微软新加了一条规则:
December 20, 2023: Confirmed out of scope - vulnerabilities in OMI or open-source components.
彼时还没有完全不认这些组件的问题, 但是后面就完全不认了. 在2024年8月, 微软再次强调了它:
August 5, 2024: Clarified open-source out of scope exclusion.
以上提到的漏洞有不少是在2024年提交的, 所以有一部分被赖掉了.
总结
有好目标就奋力挖, 别停下.
用技巧挖洞, 又快又省心
试一试, 反正不亏.
至此, 由于微软不再认可开源软件的安全漏洞, 我的 Azure 挖掘之路就告一段落了. 如果你仔细翻找azure的产品及说明文档 , 或许你们也可以在in scope范围内, 开辟自己的黄金通道.