目录
一.概述
在kamailio中用redis_cmd对redis服务器发送命令,上一篇文档介绍了redis_cmd底层通过RESP协议向服务器端发送命令,以及服务器端通过RESP解析命令,使得脚本中调用这个函数也是安全的,不会产生攻击注入。
那么脚本中是否存在mysql攻击注入问题呢?
先回答结论,答案是不存在。因为kamailio封装的mysql相关的操作底层会调用一个官方规定的安全API:mysql_real_escape_string,所以这就保证了脚本中调用的mysql相关的操作都是安全的。
接下来会从函数具体调用链路(第二部分),官方给出关于mysql_real_escape_string函数的介绍和客户端安全编程指南(第三部分),测试脚本中的函数(第四部分)来验证这个结论。
二.核心函数调用链路
1.脚本中如何找到kamailio封装的mysql函数
如何找到脚本中调用kamailio封装好的mysql命令呢?
在具体查找之前,首先要介绍一下函数之间的关系以及重要的结构体。
(1)在modules/db_mysql/km_db_mysql.c文件中,有一个db_mysql_bind_api函数,这个函数用底层的数据库函数对kamailio中封装的数据库操作进行初始化。把mysql的具体实现(比如db_mysql_query等)绑定到db_func_t对应的字段上。如下图所示:

(2)db_func_t结构体
这里的dbb是一个db_func_t结构体,关于这个结构体也需要介绍一下,它的具体位置在lib/srdb1/db.h文件中,定义了db_func_t这个结构体,厘米有很多数据库操作,比如query,insert,update等等。
db_func_t结构体是kamailio数据库接口的统一抽象层,他实现了业务模块(gs_mod.c)只需要调用db_func_t的接口函数(比如.query),不用关心到底是哪种数据库。如下图所示:

所以根据上面两个内容的介绍,再和脚本关联起来,可以初步得到这样的关系,如下图所示:

2.这些函数会执行哪些mysql操作
在mod.c文件中需要找他们的具体实现,要查看调用了哪些操作,这些操作是否是安全的。
在这里用gs_group_to_user_mapping函数来举例。
它的调用过程是脚本中调用gs_group_to_user_mapping->_gs_group_to_user_mapping,在_gs_group_to_user_mapping函数中执行了gs_appinfo_dbf.query,gs_appinfo_dbf.use_table,所以就可以知道gs_group_to_user_mapping函数底层实现了query和use_table操作。其他的函数也是这样的思路,就不逐一说明了。
所以到现在就需要验证的是use_table,free_result,query,raw_query是否安全。
use_table它的操作是固定表名,不涉及用户数据的传入。
free_result他是资源回收,同样不涉及用户传入的数据。所以这两个操作是安全的。
query是数据库查询,在下一部分详细介绍。
3.query操作的安全验证
gs_appinfo_dbf.query这个操作,在db_mysql_bind_api中执行了"dbb->query = db_mysql_query;",db_mysql_query通过查看源码可以看到是调用了db_mysql_val2str的

db_mysql_val2str函数会调用mysql_real_escape_string进行转义(这个函数官方文档介绍了是安全的API接口,使用它就可以防止SQL注入),所以至此就可以确定除了gs_enable_call_access_control之外的函数是绝对安全的,不会出现SQL注入问题。
(为什么mysql_real_escape_string这个API是安全的,在第三部分会通过官方给出的内容来验证。)

最后来用一个函数 gs_group_to_user_mapping举例整个流程是怎样的,调用关系是:
脚本调用 → gs_group_to_user_mapping → _gs_group_to_user_mapping → gs_appinfo_dbf.query(实际是 db_mysql_query)→ db_do_query → db_mysql_val2str → mysql_real_escape_string(最终转义)
(1)在 gs_group_to_user_mapping 中:
从脚本传入的 param1-param4,通过 fixup_get_svalue 提取为 val_s_1-val_s_4(str 类型,包含原始用户输入的内容和长度);
再通过 memcpy 将这些原始输入拷贝到 group_username、group_domain、domain、displayname 这四个缓冲区中;
最终调用 _gs_group_to_user_mapping,将这四个缓冲区的值传递进去
(2)在 _gs_group_to_user_mapping 中:
接收传递过来的 group_username、group_domain、domain,将它们封装到 db_val_t 类型的 query_vals 数组中;
最终将 query_vals(包含所有用户输入参数)作为参数,传入 gs_appinfo_dbf.query------ 这里的 query_vals 中的参数还是未转义的原始值,转义还未发生。
(3)函数指针的跳转:
db_mysql_bind_api 中,已经将 query 接口赋值为 db_mysql_query,因此 gs_appinfo_dbf.query 的实际执行函数是 db_mysql_query;
db_mysql_query 的参数列表完全承接了 _gs_group_to_user_mapping 传入的参数,其中核心的用户输入参数就是 _v(对应 query_vals,未转义的原始参数数组);
db_mysql_query 本身不做任何业务处理,直接调用 db_do_query,并将转义函数 db_mysql_val2str作为参数传入 db_do_query,告诉 db_do_query"请使用 db_mysql_val2str 来处理参数转义"。

(4)db_do_query 是 Kamailio 数据库驱动的通用查询逻辑函数,它遍历 _v(query_vals)中的所有参数,对需要转义的参数(如字符串类型)调用传入的转义函数 db_mysql_val2str:
遍历 query_vals 数组;
对每个参数,判断其类型(如 DB1_STR、DB1_INT),DB1_INT 类型参数无需转义(如 deleted=0),DB1_STR 类型参数需要转义;
对 DB1_STR 类型参数,调用 db_mysql_val2str,并传递四个核心参数:
_c:数据库连接句柄(用于 mysql_real_escape_string 获取当前连接的字符集);
_v:当前遍历到的 db_val_t 类型参数(包含原始用户输入,如 group_username 的值);
_s:空的缓冲区(用于存储转义后的字符串);
_len:缓冲区的长度(用于判断缓冲区是否足够,避免溢出);
等待 db_mysql_val2str 执行完成,返回转义后的字符串和长度,再将转义后的参数拼接进最终的 SQL 语句。
(5)db_mysql_val2str 处理参数,调用 MySQL 原生转义 API

(6)mysql_real_escape_string 完成最终字符转义
由 MySQL 原生 C 库实现,对原始用户输入中的特殊字符进行转义:
接收 db_mysql_val2str 传递的原始用户输入(如 "1' OR 1=1");
按照 MySQL 官方规则,对特殊字符进行转义:' → \'、\ → \ 等;
转义后的结果(如 "1\' OR 1=1")存入 _s 缓冲区;
返回转义后的字符串长度,供 db_mysql_val2str 调整缓冲区指针。
三.mysql_real_escape_string函数
1.客户端编程安全指南
在官方文档中有一个:客户端编程安全指南,链接是:https://dev.mysql.com/doc/refman/8.4/en/secure-client-programming.html
介绍了防止SQL注入的方法,其中就有mysql_real_escape_string

2.函数官方文档
官方对于这个API的介绍链接:https://dev.mysql.com/doc/c-api/8.4/en/mysql-real-escape-string.html
转义的目的是生成合法的 SQL 字符串,确保这些特殊字符不会被 MySQL 当作 SQL 语句的逻辑部分。

四.测试
1.普遍性测试
测试gs_user_to_group_mapping函数
正常情况下
$var(result) = gs_user_to_group_mapping("$tU", "$td", "$var(new_groupusername)", "$var(new_groupdomain)");
mysql数据包中的内容是:
SELECT group_username, group_domain
FROM user_group_mapping
WHERE username='10001'
AND domain='sip-gatex.gdms.cloud'
AND deleted=0
(1)测试单引号
由于tU,td都是只读变量,在脚本中无法修改,所以在这里设置test1,test2传进函数。
现在在脚本中加入注入并且打印日志,要观察是否执行了对应的函数。如下图所示:

xlog("L_INFO", "=== 注入测试代码已开始执行gs_user_to_group_mapping函数 ===");
$var(test1) = "sip-gatex.gdms.cloud' OR '1'='1";
$var(test2) = "10001";
xlog("L_INFO","gs_enable_call_access_control执行前test1的值是$var(test1),test2的值是$var(test2)");
$var(result) = gs_user_to_group_mapping("$var(test1)", "$var(test2)", "$var(new_groupusername)", "$var(new_groupdomain)");
xlog("L_INFO", "=== 注入测试代码执行完毕gs_user_to_group_mapping函数 ===");
xlog("L_INFO","gs_enable_call_access_control执行后test1的值是$var(test1),test2的值是$var(test2)");
先查看服务器的日志:

接着查看mysql的日志:

可以看到单引号被转义为\',' OR '1'='1'变成了字符串的一部分。整个字符串 sip-gatex.gdms.cloud' OR '1'='1被当作一个完整的用户名。没有SQL注入成功,只是查找一个奇怪的用户名。所以可以保证调用了query的函数是安全的。
(2)将test1,test2改为如下内容
$var(test1) = "sip-gatex.gdms.cloud'; DROP TABLE user_group_mapping; -- ";
$var(test2) = "10001' UNION SELECT username,password FROM subscriber -- ";
查看MySQL日志内容:

输入的:sip-gatex.gdms.cloud'; DROP TABLE user_group_mapping; ---
实际执行的:sip-gatex.gdms.cloud\'; DROP TABLE user_group_mapping; ---
'变成了 \'。UNION注入失败,原本想联合查询获取用户名和密码,实际效果:domain字段值为 "10001\' UNION SELECT username,password FROM subscriber --- "
也就是说在这个场景下,如果注入成功会删除表,但是根据实际结果看查找了奇怪的用户名。单引|号前的反斜杠\'使得'不再是SQL语句的结束符,而是字符串的一部分。
五.结论
综上所述,在kamailio里面,mysql的注入也不会成功,原因是调用了mysql官方的安全防注入客户端API:mysql_real_escape_string。
redis的注入不会成功的原因是客户端发送命令和服务端解析命令的时候都遵循RESP协议,会将第一个元素识别为Redis命令名,后续的所有内容无论是含(;/\n)都会当作当前命令的参数,不会被解析为独立的命令。天然具备防御命令注入的能力。