[BE101] 資訊安全


Posted by s103071049 on 2021-06-21

發現問題:明文密碼

現代人通常是一組密碼打天下,這也解釋為甚麼很多網站不是存明碼,因為一旦資料庫被攻破,駭客可以拿著明碼去試其他網站,

也因此,當你忘記密碼,他無法寄密碼回來給妳。因為資料庫中存的並不是你的明碼。

加密與 hash,不要再搞錯了,求你

加密可以解密、雜湊不行還原。

雜湊 hash(多對一)

明文 => hash => 文字,對不同的明文 hash 可能得到相同的文字,因為多對一所以無法還原。

我們可以用鴿籠原理去想像這樣的多對一關係

hash 演算法舉例:

hash 演算法 = n % 999
2 => hash => 2
1002 => hash => 2
2000 => hash => 2

不同的明文對應相同的文字,叫做碰撞。一般來說,有名的演算法發生碰撞的機率都很低。

hash 機制可以運用在密碼上,登入後輸入的密碼與資料庫 hash 過後的結果進行比對。

延伸閱讀
[資訊安全] 密碼存明碼,怎麼不直接去裸奔算了?淺談 Hash , 用雜湊保護密碼
一次搞懂密碼學中的三兄弟 — Encode、Encrypt 跟 Hash

修正問題:使用內建 hash 函式

password_hash 可以選幾種不同的演算法,要注意較舊的 php 版本不支援。

到註冊處理頁面,調整 password

$password = password_hash($_POST['password'], PASSWORD_DEFAULT);

利用 password_verify 進行密碼驗證,password_verify(傳入的密碼,驗證後的結果)

登入處理

<?php
  require_once('conn.php');
  session_start();
  $username = $_POST['username'];
  $password = $_POST['password'];

  if (empty($username) || empty($password)) {
    header('Location: login.php?errCode=1');
    die('資料未輸入齊全');
  }

  $sql = sprintf('select * from users where username="%s"', $username);
  $result = $conn -> query($sql);
  if ($result->num_rows === 0) {
    header("Location: login.php?errCode=2");
    exit();
  }

  $row = $result->fetch_assoc();
  if (password_verify($password, $row['password'])) {
    $_SESSION['username'] = $username;
    header('Location: main.php');
  } else {
    header('Location: login.php?errCode=2');
    die('帳密輸入錯誤');
  }
?>

註冊處理

<?php
  require_once('conn.php');
  session_start();
  $username = $_POST['username'];
  $nickname = $_POST['nickname'];
  $password = password_hash($_POST['password'],PASSWORD_DEFAULT);
  if (empty($username) || empty($nickname) || empty($password)) {
    header('Location: register.php?errCode=1');
    die('資料未輸入完整');
  }
  $sql = sprintf('insert into users(username, nickname, password) values("%s", "%s", "%s")', $username, $nickname, $password);
  $result = $conn -> query($sql);
  if (!$result) {
    $code = $conn -> errno;
    if ($code === 1062) {
      header('Location: register.php?errCode=2');
    }
    die($conn->error);
  }
  $_SESSION['username'] = $username; //這樣註冊完成就是登入狀態
  header('Location: main.php');
?>

重點回顧

  1. password 註冊時會經過 hash
  2. 登入時,會先透過 username 將 password 撈出來,所以會先檢查有無查到 user 沒有的話會錯誤返回。如果有拿到 password 將 password 拿出來,再經過 password_verify 去錯誤比對。
  3. password_verify 比對使用者輸入的 password 與資料庫存的 password。如果通過驗證就登入成功,否則登入失敗

優點:資料庫被偷,保護使用者的密碼

發現問題:XSS

Cross-site Scripting 在別的網站上(跨站)執行 script。舉例來說,目前留言版輸入的東西,並不是字串,會被解讀成程式碼的一部分。也就是我可以寫一串 js 將他導入釣魚網站,做一個跟原本網頁很像的網站。

透過 <script>console.log(document.cookie)</script> 可以拿到 session_id。透過這樣的方式可以拿到這個頁面使用者的 cookie ,再利用其他方式傳到我自己的 server。偷他 cookie 就可以偷他 session_id,偷他 session_id 就可以偷他身分。

修正問題:htmlspecialchars

會將一些特殊字元進行編碼。

將跳脫的內容存到資料庫,IOS、安卓看不懂這內容。所以應該要保留使用者的原貌,在顯示的時候做這件事情。要將所有使用者可以自己調控內容的地方,都做 escape。之後如果出現新的欄位,也要做這件事情。

main.php

<div class='card__text'>
    <div class='card__text-title'>
     <span class='nickname'><?php echo escape($row['nickname'])?>  </span>
     <span class='date'><?php echo $row['create_at']?></span>
    </div>
    <div class='card__text-content'>
         <?php echo escape($row['content'])?>
     </div>
</div>

utils.php

  function escape($str) {
    return htmlspecialchars($str, ENT_QUOTES);
  }

發現問題:SQL Injection 駭客的填字遊戲

與 xcs 類似,只是用在 sql query 上面。

select 填字

這段程式碼等同 select * from users where username = 'aa' 後面都會被當作註解。

儘管我不知道 username 我也可以做相同的事情,進行登入。

例如,我可以select * from users where username = "" or 1=1 #攻破資料庫。 1 = 1 表示 true

insert 填字

insert 可以新增多筆資料。 INSERT INTO comments(nickname, content) values('aa', 'bb'), ('cc','dd'),知道這樣的方式,可否將原本的 query 改成我想插入的方式 ?

INSERT INTO comments(nickname, content) values('aa', 'bb'), ('cc','dd'); #利用這樣的形式,可以插入多筆資料

insert into comments(nickname, content) values('%s', '%s'); # 原本的形式

#先不要管 nickname,處理 content
insert into comments(nickname, content) values('aa', '%s');

#我們可以這樣改
insert into comments(nickname, content) values('aa', ''),('admin', 'test');

#與原本的 query 比對 => 代碼意思變成我要新增兩個內容。
%s 是 '),('admin', 'test

因為我的 query 是用字串拼接,所以任何人都可以拼拼湊湊將query變成她想要的樣子。有了這個漏洞,我們就可以模仿任何人發文。

除此之外,還可以利用 sql query 的方式去攻擊資料庫,拿到所有你想拿的資料。
例如:新增一筆資料,內容就是密碼

#這麼做,掌握 query 完全部份
insert into comments(nickname, content) values('aa', ''),('admin', 'test')#'); #米字號後面都會變成註解

#subsql,sql 裡面可以是 sql
insert into comments(nickname, content) values('aa', (select password from users limit 1));
#與 SQL injection 結合
insert into comments(nickname, content) values('aa', ''),('admin', (select password from users limit 1))#'

這個時候 content 是  '),('admin', (select password from users limit 1))#

#指定撈 user_id = 88 的密碼
insert into comments(nickname, content) values('aa', ''),('admin', (select password from users where id = 88))#'

可以透過 echo $sql; 搭配 exit() 來印出拼湊完的結果。

還可以改得更複雜,將 username 一起拿到手

現在 content :
'),((select username from users where id = 88), (select password from users where id = 88))#

資料庫中會有預設的 information schema 裡面會有 sys_tables,會知道你有哪些 table。每個 table 也有一些東西可以知道那個欄位是甚麼。或者就算沒有上述,欄位的取名也差不多,也可以用猜的。

修正問題:prepared statement

用 mysql 的內建機制,做字串拼接。有點像 xcs 用字串跳脫、字串轉移的概念。透過內建機制將這些指令解釋成字串。

先處理 handle_add_comment.php,將 sprintf() 改成下列形式

$sql ='insert into comments(nickname, content) values(?, ?)';

將 sql 指令傳入 prepare,再將參數 bind 到參數上面,最後去執行這個 query

$stmt = $conn->prepare($sql);
#看我有幾個參數,就會有幾個字。s 表示 string。
$stmt->bind_param('ss', $nickname, $content);
$result = $stmt->execute();

透過這樣的作法,之前準備的惡意字串,就不會被當作指令去執行。

全部的地方都要改,除了避免漏網之魚,也是為了追求一致性。

調整 handle_register.php

  $sql = 'insert into users(username, nickname, password) values(?, ?, ?)';
  $stmt = $conn->prepare($sql);
  $stmt->bind_param("sss", $username, $nickname, $password);
  $result = $stmt->execute();

調整 handle_login.php

  $sql ='select * from users where username=?';
  $stmt = $conn->prepare($sql);
  $stmt->bind_param("s",$username);
  $result = $stmt->execute(); #只是看有無執行成功

  # 要拿到資料,必須要再加上
  $result = $stmt->get_result();

調整 utils.php

  function getNickname($username) {
    global $conn;
    $sql = 'select nickname from users where username = ?';
    $stmt = $conn->prepare($sql);
    $stmt->bind_param("s",$username);
    $result = $stmt->execute();
    $result = $stmt->get_result();
    $nickname = $result->fetch_assoc()['nickname'];
    return $nickname;
  }

調整 main.php

  $stmt = $conn->prepare('select * from comments order by id desc');
  $result = $stmt->execute();
  if (!$result) {
    die('錯誤訊息 : ' . $conn ->error);
  }
  $result = $stmt->get_result();
  $username = Null;
  $nickname = Null;
  if (!empty($_SESSION['username'])) {
    $username = $_SESSION['username'];
    $nickname = getNickname($username);
  }

#PHP #SQL #db-attack #Security







Related Posts

Vite 怎麼能那麼快?從 ES modules 開始談起

Vite 怎麼能那麼快?從 ES modules 開始談起

《鳥哥 Linux 私房菜:基礎篇》Chapter 05 - Linux 的檔案權限與目錄配置

《鳥哥 Linux 私房菜:基礎篇》Chapter 05 - Linux 的檔案權限與目錄配置

React Native in 24 Hours

React Native in 24 Hours


Comments