Entr'ouvert Lasso类型混淆漏洞分析:从SAML响应到代码执行

本文详细分析了Cisco Talos发现的Entr'ouvert Lasso库中的类型混淆漏洞(TALOS-2025-2193),该漏洞存在于lasso_node_impl_init_from_xml功能中,攻击者可通过特制SAML响应实现任意代码执行,CVSS评分高达9.8分。

TALOS-2025-2193 || Cisco Talos情报组 - 全面威胁情报

漏洞概述

Entr’ouvert Lasso库的lasso_node_impl_init_from_xml功能中存在类型混淆漏洞。特制的SAML响应可导致任意代码执行。攻击者可通过发送畸形SAML响应触发此漏洞。

受影响的版本

以下版本经Talos测试或供应商确认存在漏洞:

  • Entr’ouvert Lasso 2.5.1
  • Entr’ouvert Lasso 2.8.2

产品链接

Lasso - https://lasso.entrouvert.org/

CVSSv3评分

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-843 - 使用不兼容类型访问资源(类型混淆)

技术细节

Lasso SAML库是安全断言标记语言(SAML)标准的开源实现,主要用于在Web应用程序中启用单点登录(SSO)功能。它提供了SAML身份验证、处理断言、元数据解析以及服务提供商(SP)和身份提供商(IdP)交互的工具。

在解析攻击者控制的SAMLResponse时,lasso_node_impl_init_from_xml触发了一系列函数调用,导致g_hash_table_insert被调用时使用了攻击者控制的字符串,而不是预期的GHashTable*值。

从lasso/sml/xml.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1495     /* 收集特殊代码片段,如SNIPPET_COLLECT_NAMESPACES、SNIPPET_ANY、SNIPPET_ATTRIBUTE
1496      * 或SNIPPET_SIGNATURE,并反向初始化class_list */
1497     while (class && LASSO_IS_NODE_CLASS(class)) {
...
1503             for (snippet = class->node_data->snippets; snippet && snippet->name; snippet++) {
1504                 type = snippet->type & 0xff;
1505 
1506                 if (snippet->name && snippet->name[0] == '\0' && type ==
1507                         SNIPPET_COLLECT_NAMESPACES) {
1508                     snippet_collect_namespaces = snippet;
1509                     g_type_collect_namespaces = g_type;
1510                 } else if (type == SNIPPET_SIGNATURE) {
1511                     snippet_signature = snippet;
1512                 } else if (type == SNIPPET_ATTRIBUTE && snippet->type & SNIPPET_ANY) {
1513                     g_type_any_attribute = g_type;
1514                     snippet_any_attribute = snippet;   // 此处

注意,具有SNIPPET_ATTRIBUTE类型并与SNIPPET_ANY掩码的代码片段在第1514行被保存到snippet_any_attribute中供后续使用。

随后,发生类似的循环:

 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
1561         for (class_iter = class_list; class_iter; class_iter = class_iter->next) {
1562             class = class_iter->data;
1563             for (snippet = class->node_data->snippets;
1564                     snippet && snippet->name; snippet++) {
1565                 type = snippet->type & 0xff;
1566                 /* 如果属性名称与代码片段名称相同,则分配属性内容,且:
1567                  * - 代码片段和属性都没有命名空间
1568                  * - 代码片段没有命名空间但属性具有与节点相同的命名空间
1569                  * - 代码片段和节点都有命名空间,且相等
1570                  */
1573                 if (type != SNIPPET_ATTRIBUTE)
1574                     continue;
1575                 if (! lasso_strisequal((char*)attr->name, (char*)snippet->name))
1576                     continue;
1577                 if (attr->ns) {
1578                     gboolean same_namespace, given_namespace;
1579 
1580                     same_namespace = lasso_equal_namespace(attr->ns,
1581                             xmlnode->ns) && ! snippet->ns_uri;
1582                     given_namespace = snippet->ns_uri &&
1583                         lasso_strisequal((char*)attr->ns->href,
1584                                 snippet->ns_uri);
1585                     if (! same_namespace && ! given_namespace)
1586                         break;
1587                 }
1588                 snippet_set_value(node, class, snippet, content); // 发生strdup调用的地方
1589                 ok = 1;
1590                 break;
1591             }

之前保存在snippet_any_attribute中的相同代码片段被传递给snippet_set_value。

 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
1338 static void     
1339 snippet_set_value(LassoNode *node, LassoNodeClass *class, struct XmlSnippet *snippet, xmlChar *content) {
1340     void *value;        
1341     GType g_type = G_TYPE_FROM_CLASS(class);
1342                     
1343     /* 如果没有偏移量,意味着由临时init_from_xml处理 */
1344     if (! snippet->offset && ! (snippet->type & SNIPPET_PRIVATE)) {
1345         return;
1346     }
1347     value = SNIPPET_STRUCT_MEMBER_P(node, g_type, snippet);
1348     if (snippet->type & SNIPPET_INTEGER) {
1349         int val = strtol((char*)content, NULL, 10);
1350         if (((val == INT_MIN || val == INT_MAX) && errno == ERANGE)
1351                 || errno == EINVAL || val < 0) {
1352             if (snippet->type & SNIPPET_OPTIONAL_NEG) {
1353                 val = -1;
1354             } else {
1355                 val = 0;
1356             }
1357         }
1358         (*(int*)value) = val;
1359     } else if (snippet->type & SNIPPET_BOOLEAN) {
1360         int val = 0;
1361         if (strcmp((char*)content, "true") == 0) {
1362             val = 1;
1363         } else if (strcmp((char*)content, "1") == 0) {
1364             val = 1;
1365         }   
1366         (*(int*)value) = val;
1367     } else {    

1369         if (lasso_flag_memory_debug == TRUE) {
1370             fprintf(stderr, "   setting prop %s/%s to value %p: %s\n",
1371                     G_OBJECT_TYPE_NAME(node), snippet->name, *(void**)value, (char*)content);
1372         }       
1373     }

然后,代码片段类型在第1367-1372行被当作字符串处理。strdup分配一个包含有效负载的新缓冲区,指向该缓冲区的指针被写入value的内存地址。

最后,在上述循环完成后,代码检查snippet_any_attribute是否被设置为任何值(第1593行,如下所示)。如果是,则处理该代码片段。第1597行与上面的第1347行是相同的操作,因此any_attribute指向value曾经指向的相同内存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1593         if (! ok && attr->ns && snippet_any_attribute) {
1594             GHashTable **any_attribute;
1595             gchar *key;
1596 
1597             any_attribute = SNIPPET_STRUCT_MEMBER_P(node, g_type_any_attribute,
1598                     snippet_any_attribute);
1599             if (*any_attribute == NULL) {
1600                 *any_attribute = g_hash_table_new_full(g_str_hash, g_str_equal,
1601                         g_free, g_free);
1602             }
1603             if (lasso_equal_namespace(attr->ns, xmlnode->ns)) {
1604                 key = g_strdup((char*)attr->name);
1605             } else {
1606                 key = g_strdup_printf("{%s}%s", attr->ns->href, attr->name);
1607             }
1608             g_hash_table_insert(*any_attribute, key, g_strdup((char*)content)); // 崩溃

由于上面调用了snippet_set_value,在那里放置了一个指向字符串的指针,它不会为null。因此,在第1608行调用了g_hash_table_insert,使用了strdup结果,它认为这是一个GHashTable*,但这实际上是攻击者控制的字符串。

最终,这导致在攻击者控制的地址上执行调用指令。

1
2
3
4
0x7ffff7ec09fa <g_hash_table_insert+001a> je     0x7ffff7ec0b00 <g_hash_table_insert+288>
0x7ffff7ec0a00 <g_hash_table_insert+0020> mov    rbp, rdi
0x7ffff7ec0a03 <g_hash_table_insert+0023> mov    rdi, rsi
0x7ffff7ec0a06 <g_hash_table_insert+0026> call   QWORD PTR [rbp+0x38]

攻击者发送精确构造的畸形SAML响应可在g_hash_table_insert操作期间导致类型混淆,最终可能导致远程代码执行。

崩溃信息

1
2
3
4
5
6
7
8
==466583== Jump to the invalid address stated on the next line
==466583==    at 0x4242414141414141: ???
==466583==    by 0x4885643: lasso_node_init_from_xml (xml.c:717)
==466583==    by 0x488AD23: lasso_node_new_from_xmlNode_with_type (xml.c:2492)
==466583==    by 0x488AA36: _lasso_node_new_from_xmlNode (xml.c:2417)
==466583==    by 0x488AC4E: lasso_node_new_from_xmlNode_with_type (xml.c:2478)
==466583==    by 0x4888635: lasso_node_impl_init_from_xml (xml.c:1712)
==466583==    by 0x4885643: lasso_node_init_from_xml (xml.c:717)

时间线

  • 2025-05-13 - 初始供应商联系
  • 2025-05-14 - 供应商披露
  • 2025-08-12 - 供应商补丁发布
  • 2025-11-05 - 公开发布

致谢

由Cisco高级安全倡议组的Keane O’Kelley和另一名成员发现

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计