簡單記錄幾個最近碰到的神奇特性,直接講不有趣,先來幾個小挑戰:
挑戰一
猜猜底下程式碼的執行結果是什麼?
1 | var regexp = /huli/g |
挑戰二
先讓你輸入一個密碼,然後讓你輸入一段程式碼,可以拿到已經不見的變數嗎?
1 | var password = prompt('input password') |
挑戰三
底下的寫法會出事嗎?會的話是出什麼事?怎麼觸發?
1 | var tmpl = '<input type="submit" value="{{value}}">' |
有狀態的 RegExp
猜猜底下程式碼的執行結果是什麼?
1 | var regexp = /huli/g |
無論是誰來看都會覺得兩個都是 true 吧?但答案是 true 跟 false,甚至你寫成這樣,第二個也是 false:
1 | var regexp = /huli/g |
會有這樣的結果,是因為 RegExp 是 stateful 的,如果有 global 或是 sticky 的 flag 的話。
RegExp 有一個 lastIndex
的屬性,會記錄上次符合的位置,下次再使用 test
時就會從 lastIndex
開始找起。如果找不到的話,lastIndex
會自動歸零。
1 | var regexp = /huli/g |
所以根據上面所講的 lastIndex
的特性,這樣乍看之下是沒問題的:
1 | var regexp = /huli/g |
但並不代表沒有 bug。
上面這一段之所以看起來沒問題,只是因為第一次找完以後 lastIndex
是 4,而剛好 str2 中 huli 出現的位置是從 5 開始,所以一樣找得到,如果把最後兩行位置對調,就會產生預期外的結果。
總之呢,在使用 global RegExp 的時候要小心這個特性。而對資安來說,則是可以關注這些潛在的 bug,看看有沒有能利用的地方。
RegExp 的神奇紀錄屬性
延續開頭的小挑戰:
1 | var password = prompt('input password') |
變數已經被清空了,所以是拿不到變數的。
但我們可以靠著 RegExp 上的一個神奇屬性來拿到,叫做:RegExp.input,這個屬性會紀錄上一次 regepx.test()
符合時的 input:
1 | /hello/.test('hello world') |
除此之外,還有其他參數也會被記錄:
- RegExp.lastMatch ($&)
- RegExp.lastParen ($+)
- [RegExp.leftContext ($`)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/leftContext)
- RegExp.rightContext ($’)
第一次知道這技巧是在 DiceCTF 2022 - web/nocookies
RegExp 的特殊變數
開頭的挑戰三中我們給出了底下這段程式碼:
1 | var tmpl = '<input type="submit" value="{{value}}">' |
雙引號被濾掉了,所以照理來說應該沒辦法跳脫出屬性才對,>
也被拿掉了,所以也沒辦法關閉標籤。
但是呢,在做字串取代的時候,有種東西叫做:special replacement patterns,舉例來說 $` 可以拿到字串取代的地方的「前面」,$'
則是可以拿到後面,看個範例會更容易理解:
1 | const str = '123{n}456' |
因此回到我們的題目:
1 | var tmpl = '<input type="submit" value="{{value}}">' |
{{value}} 的後面是 ">
,雖然這兩個字元都被過濾掉,但我們可以用 $'
來拿到這兩個字元。
因此這題的答案是 $'<style onload=alert(1)
:
1 | var tmpl = '<input type="submit" value="{{value}}">' |
先用 $'
也就是 ">
來關閉標籤,就可以用其他標籤進行 XSS,最後產生的結果是:
1 | <input type="submit" value=""><style onload=alert(1) "> |
我第一次知道這個是在 PlaidCTF 2022 - YACA,但在 DragonCTF 2021 - Webpwn 中似乎也出現過類似的技巧。