去挑几个好玩的漏洞来分析一下
https://www.cvedetails.com/vulnerability-list/vendor_id-74/product_id-128/year-2022/PHP-PHP.html
https://github.com/CFandR-github/PHP-binary-bugs/blob/main/cve_2022_31626_remote_exploit/cve_writeup.md
开始学习吧
内存申请不够导致边界溢出 那么入手点就在MYSQLND_HEADER_SIZE
这个参数,看名字应该是mysqlnd通讯时的头部
poc测试
poc倒是没有给出shell什么的 但是的确是让服务crash了
环境 1 2 chocolazy@chocolazy:~/php74-dev$ cat /proc/version Linux version 5.4.0-91-generic (buildd@lcy01-amd64-017) (gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)) #102-Ubuntu SMP Fri Nov 5 16:31:28 UTC 2021
1 2 3 4 5 6 7 8 git clone https://github.com/php/php-src cd php-src/ git switch PHP-7.4.30 git checkout 55f6895f4b4c677272fd4ee1113acdbd99c4b5ab cp -R php-src/ php74- cd php74-dev/ ./buildconf './configure' '--with-bz2' '--with-zlib' '--with-mysqli=mysqlnd' '--enable-pdo' '--with-pdo-mysql=mysqlnd' '--enable-sockets' '--with-curl'
为了避免麻烦 跟参考文章相比 configure里删了一些没用的 然后就是报错一个安装一个 比如
1 2 configure: error: bison 3.0.0 is required to generate PHP parsers (excluded versions: none). chocolazy@chocolazy:~/php74-dev$ sudo apt install bison
把优化关了
然后 make -j4
一下 等…
再配置一个clion的调试 https://blog.csdn.net/weixin_42264234/article/details/120937676 完成
分析与调试 我们需要知道的是 文章对漏洞本身的分析一笔带过 而花了很大一部分去布局
抛弃文章的分析 如何定位到问题点
测试脚本
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 <?php function select ($result , $limit = 0 ) { for ($i = 0 ; (!$limit || $i < $limit ) && ($row = $result ->fetch_row ()); $i ++) { if (!$i ) { echo "<div class='scrollable'><table cellspacing='0' class='nowrap'><thead>" ; for ($j = 0 ; $j < count ($row ); $j ++) { $field = $result ->fetch_field (); $name = $field ->name; $orgtable = $field ->orgtable; $orgname = $field ->orgname; echo "<th" . ($orgtable != "" || $field ->name != $orgname ? " title='" . ($orgtable != "" ? "$orgtable ." : "" ) . $orgname . "'" : "" ) . ">" . $name . '' ; } echo "</thead>" ; } echo "<tr>" ; foreach ($row as $key => $val ) { echo "<td>" ; echo $val ; echo "</td>" ; } echo "</tr>" ; } echo ($i ? "</table></div>" : "" ); } class MyDB extends MySQLi { var $extension = "MySQLi" ; function __construct ( ) { parent ::init (); } function connect ($server = "" , $username = "" , $password = "" , $database = null , $port = null , $socket = null ) { $return = @$this ->real_connect ($server , $username , $password , $database , $port ); $this ->options (MYSQLI_OPT_LOCAL_INFILE, false ); return $return ; } } $s = [ 'server' => '192.168.28.1' , 'username' => 'root' , 'password' => 'hundan' , 'db' => 'test' , 'port' => 3306 , ]; $c = new MyDB ();$c ->connect ($s ['server' ], $s ['username' ], $s ['password' ], $s ['db' ], $s ['port' ]);$c ->multi_query ("select 1; select 2;" );do { $result = $c ->store_result (); if (is_object ($result )) { select ($result ); } } while ($c ->next_result ());
1 gdbserver :1234 ../../sapi/cli/php m.php
检查补丁 漏洞点位于 ext/mysqlnd/mysqlnd_wireprotocol.c:774
直接打断点进不去 根据跟踪我们找到了两个入口
1 2 ext/mysqlnd/mysqlnd_auth.c:273 ext/mysqlnd/mysqlnd_auth.c:403
我们选择前者进行分析 其中 use_full_blown_auth_packet
向上跟踪 ext/mysqlnd/mysqlnd_auth.c:147
有个first_call = FALSE; 根据上下文理解应该不是明文登录 而是可能类似断线重连的操作
我们看到 ext/mysqlnd/mysqlnd_auth.c:163
1 while (ret == FAIL && conn->error_info->error_no == 0 && switch_to_auth_protocol != NULL);
switch_to_auth_protocol
需要再跟进去 找到这一段 ext/mysqlnd/mysqlnd_auth.c:334
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (auth_resp_packet.response_code == 0xFE ) { if (!auth_resp_packet.new_auth_protocol) { DBG_ERR(mysqlnd_old_passwd); SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE, mysqlnd_old_passwd); } else { *switch_to_auth_protocol = mnd_pestrndup(auth_resp_packet.new_auth_protocol, auth_resp_packet.new_auth_protocol_len, FALSE); *switch_to_auth_protocol_len = auth_resp_packet.new_auth_protocol_len; if (auth_resp_packet.new_auth_protocol_data) { *switch_to_auth_protocol_data_len = auth_resp_packet.new_auth_protocol_data_len; *switch_to_auth_protocol_data = mnd_emalloc(*switch_to_auth_protocol_data_len); memcpy (*switch_to_auth_protocol_data, auth_resp_packet.new_auth_protocol_data, *switch_to_auth_protocol_data_len); } else { *switch_to_auth_protocol_data = NULL ; *switch_to_auth_protocol_data_len = 0 ; } } }
我们在登录报文里找到了response code
在mysql文档里面我们找到了关于ok packet的信息 也就是说0xFE代表的是EOF数据包
但我们还是不去纠结这些信息在mysql
中代表什么 但是简单来说我们需要在auth user
阶段去构造一个eof
我们尝试去构造一个畸形的packet
为了节省额外工作的时间 我们直接去观察一下 rogue_sql_server.py
cve_2022_31626_remote_exploit/rogue_sql_server.py:276
1 2 #switch auth packet p2 = AuthSwitch()
cve_2022_31626_remote_exploit/rogue_sql_server.py:188
1 2 3 4 5 6 7 8 9 10 11 12 13 class AuthSwitch(Packet): def __init__(self): self.status = 0xfe self.auth_method_name = 'mysql_clear_password' self.auth_method_data = 'abc' def get_to_str(self): r = '' r += self.pack_1_byte(self.status) r += self.auth_method_name + '\x00' r += self.auth_method_data + '\x00' return r
返回成功后我们进入到漏洞点 ext/mysqlnd/mysqlnd_wireprotocol.c:774
我们仔细观察一下 尝试让代码成立
1 pfc->cmd_buffer.length >= packet->auth_data_len
向上跟踪 当然大概率 cmd_buffer
是写死的 如果 cmd_buffer.length
可控的话 那么将是一个莫大的喜讯 而 auth_data_len
是我们能控制的位置
由于结构体等定义在头文件里无法动态调试 我们又翻阅起了鸟哥的源码解析 https://www.laruence.com/2020/03/23/5605.html
到这里我们就能明白 我们很难通过获取数据库服务器去反向攻击代码服务器 更多的是通过类似pma的情况
一些其他备注 Z_OBJ_P zend_object_get_address
XtOffsetOf(zend_string, val)
表示计算出zend_string结构体的大小
https://www.sudytech.com/_s80/_t690/2018/0729/c3276a26052/page.psp
https://gywbd.github.io/posts/2016/2/debug-php-source-code.html
http://c.biancheng.net/view/446.html
https://blog.csdn.net/weixin_42264234/article/details/120937676#t6
https://www.jiyik.com/tm/xwzj/prolan_290.html
https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_ok_packet.html
https://www.cnblogs.com/pingyeaa/p/9688248.html
https://www.codeleading.com/article/3528741432/
https://www.laruence.com/2020/03/23/5605.html