Impossible level源代码
<?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();
?>
代码审计
审计checkToken
首先,该代码片段使用了 checkToken()
函数来验证 Anti-CSRF 令牌,即检查用户的访问令牌是否有效,以防止CSRF。
CSRF原理
CSRF(Cross-Site Request Forgery,跨站点请求伪造)是一种利用 Web 应用程序身份验证漏洞的攻击方式,攻击者通过伪造请求来冒充受信任用户的身份,以执行某些未经授权的操作。这种攻击方式通常发生在 Web 应用程序中存在漏洞的情况下,而攻击者则可以利用这些漏洞来伪造请求、窃取用户会话信息和执行非授权的操作。
例如,假设某个在线银行应用程序使用 GET 请求来执行转账操作,然后攻击者在一个恶意网站上嵌入一个包含一个类似于以下代码的图像:
<img src="http://bank.com/transfer?to=attacker&amount=100" />
如果受害者浏览器中已经登录到银行的账户,并且在同一浏览器中访问了上面的恶意网站,那么这个 GET 请求就会被发送到银行应用程序中,并转移 100 美元给攻击者。由于该请求与受害者浏览器中的 cookie 相关联,并且被认为是合法的请求,因此银行应用程序将无法区分攻击者和受害者之间的差异。
为了防止 CSRF 攻击,Web 应用程序通常会生成一个随机的 CSRF 令牌,并将其包含在每个表单请求、链接和 AJAX 请求中。这个令牌可以告诉应用程序这个请求是否是受信任的,因为令牌只有在用户通过 Web 应用程序正常交互提交请求时才会出现。
审计is_numeric
从 GET 请求参数中获取 id
值,并使用 is_numeric()
函数检查该值是否为数字。如果是数字,则使用 intval()
函数将其转换为整数类型,然后根据配置文件中的 SQLI_DB
常量所指定的数据库类型(MySQL 或 SQLite),进行相应的查询操作
浮点数精度原理
在数据库中,整数类型和小数类型的自增方式不同。整数类型的自增方式是在原有的基础上加1,而小数类型则是在原有的基础上加上指定的增量。
假设在一个 static
表中,使用 FLOAT
类型的 id
字段作为主键,并且其初始值为 1.0
,递增步骤为 0.1
。当插入记录时,系统自动生成的 ID 值如下所示:
id | name | price |
---|---|---|
1.0 | A | 1 |
1.1 | B | 2 |
1.2 | C | 3 |
1.3 | D | 4 |
1.4 | E | 5 |
由于浮点数采用的是二进制表示法,而非十进制,因此可能存在像 0.1 这样的十进制小数无法精确转化为二进制的情况。所以实际的 ID 值可能是 1.0999999999
或者 1.1000000001
等类似的近似值。在这种情况下,可能会出现重复数据。
比如,在向 statict
表中插入一条记录时,新记录的 ID 值计算结果为 1.2000000001
,这个值与已经存在的 ID 值 1.2
可能会相同,导致数据库插入失败。
另外,若出现数据查找或更新等操作时,可能会因为 ID 不准确而无法正确操作记录,例如查询 ID 为 1.2999999999
的记录,实际上这条记录不存在,但是由于精度问题,可能会得到一条随机的记录。
这种精度问题可能会导致小数值被截断或近似值导致的数据不一致。攻击者可以通过构造恶意的请求,将输入的小数值进行微调,进而获取到本来不应该被暴露的数据。
审计占位符:id
在MYSQL中,使用 PDO 对象的 prepare()
方法准备 SQL 查询语句,在其中使用占位符 :id
代替 id
参数,以避免 SQL 注入攻击。
占位符防sql原理
举例如下:
假设某注入语句为SELECT * FROM users WHERE id=1 OR 1=1
,这将返回所有用户记录。
如果使用占位符 :id代替id参数,则该查询语句将变为 SELECT * FROM users WHERE id=:id
,查询语句将不会受到任何注入攻击。
审计bindParam()
使用 bindParam()
方法将 id
参数绑定到占位符上,指定参数类型为整数类型。
bindParam()原理
当使用 bindParam() 方法时,参数并没有直接拼接到 SQL 语句中,而是将参数绑定到占位符上,然后统一交给数据库执行器去处理。这样可以避免通过字符串拼接方式构造 SQL 语句,从而防止 SQL 注入攻击。
假设有一个查询用户信息的 SQL 语句:
SELECT * FROM users WHERE id = :id
此时可以使用 bindParam() 方法将 id 参数绑定到占位符 :id 上,例如:
$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();
审计
使用 execute()
方法执行查询,使用 fetch()
方法获取查询结果中第一行的数据,并将其存储到 $row
数组中。然后判断 $data->rowCount()
的值是否为 1,确保只有一个结果被返回.
在 SQLite 中,使用全局变量 $sqlite_db_connection
获取 SQLite 数据库连接对象,并使用 prepare()
方法准备 SQL 查询语句,在其中使用占位符 :id
代替 id
参数。
使用 bindValue()
方法将 id
参数绑定到占位符上,指定参数类型为整数类型。
使用 execute()
方法执行查询,并使用$result->numColumns()
方法获取查询结果集的列数,如果列数为 2,则调用 $result->fetchArray() 方法获取查询结果集的第一行数据,将第一列的值赋值给 $row[0],第二列的值赋值给 $row[1],从而得到指定 user_id 的用户的 first_name 和 last_name 两个字段的值。
最后,使用 generateSessionToken()
函数在服务器端生成 Anti-CSRF 令牌,并将其存储到 $_SESSION['session_token']
变量中。
总结
以上为DVWA之SQL注入—Impossible level代码审计。
往期参考:
[网络安全]DVWA之SQL注入—low level解题详析
[网络安全]DVWA之SQL注入—medium level解题详析
[网络安全]DVWA之SQL注入—High level解题简析
后续将分享SQL-Labs攻击姿势及解题详析。
我是秋说,我们下次见。