Skip to content

Commit 01aa27d

Browse files
committed
docs: is 패키지 포스팅
1 parent bc2b271 commit 01aa27d

File tree

13 files changed

+282
-0
lines changed

13 files changed

+282
-0
lines changed
5.04 KB
Loading
4.4 KB
Loading
13.4 KB
Loading
62.4 KB
Loading
9.41 KB
Loading
77.7 KB
Loading
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
---
2+
title: 'is 패키지의 Supply Chain Attack 과정 분석'
3+
description: '메인테이너는 어떻게 토큰을 탈취당했을까?'
4+
tags: ['is', 'supply-chain-attack', 'npm']
5+
coverImage: 'cover.webp'
6+
date: '2025-09-10T11:02:32.000Z'
7+
---
8+
9+
![cover](cover.webp 'size=cover;origin=https://socket.dev/blog/npm-phishing-email-targets-developers-with-typosquatted-domain')
10+
11+
## 들어가며
12+
13+
![nodejs discord warning](node-warning.webp 'l')
14+
15+
새벽 2시반, 잠들기 직전에 Node.js 디코에서 `@everyone`이 찍혔다. 이런 경우는 디코가 생긴 이후로 최초이므로 호기심이 발동해 직관을 하게 되었다.
16+
17+
- 내용인즉 chalk, debug, color 등 을 관리하는 메인테이너의 계정이 탈취, 악성코드가 담긴 버전이 배포 됨
18+
- 영향 받는 범위는 @babel/core, express, Node.js, Next.js 등 엄청나게 컸음
19+
- 주간 다운을 합치면 20억이 넘음
20+
- 멀웨어 스크립트에는 `typeof window !== 'undefined` 코드가 있는데, CICD 환경 등에 침투하여 암호화폐 탈취를 위한 스크립트가 있었음.
21+
- 관련 링크들
22+
- [해킹당한 메인테이너의 대응이 궁금하면 여기](https://github.com/debug-js/debug/issues/1005)
23+
- https://github.com/advisories/GHSA-8mgj-vmr8-frr6
24+
- [사건 요약](https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised)
25+
26+
최근 <u>메인테이너 계정이 탈취되고 패치버전이 올라가는 문제</u>들이 많이 생기고 있다.
27+
28+
이번 포스트에서는 비슷한 경우인 `is` 패키지의 토큰 탈취 과정과 공격 코드 분석을 해보려고 한다.
29+
30+
## TL;DR: 무슨 일이 벌어졌나?
31+
32+
- [npm](https://www.npmjs.com/) 패키지 [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier)(주간다운2000만), [is](https://www.npmjs.com/package/is)(250만) 등에 악성코드가 추가됨
33+
- is 패키지는 타입 체크를 도와주는 기분적인 유틸 패키지
34+
- is.symbol(…), is.nan(…) 등<ExternalAnchor href="https://github.com/enricomarino/is/blob/cb4d31da1fe9e315e4bd917b063c636bf110e2b2/index.js#L558" />
35+
- 공격자는 이전 메인테이너의 계정을 탈취해 악성 코드가 포함된 버전을 배포
36+
- 유저가 해당 패키지를 설치하게 되면 각종 정보가 해커에게 전송 되고 실행 명령어가 그대로 노출 됨
37+
38+
## 공급망 공격이란?
39+
40+
Supply Chain Attack(공급망 공격)의 정의는 아래와 같다.
41+
42+
- 해커가 SW나 HW 개발 단계부터 악성코드를 제품의 악의적 변조 또는 제품 내부에 악성코드 등을 숨기는 공격기법
43+
- 대표적인 예로 [스턱스 넷](https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%84%B1%EC%8A%A4%EB%84%B7)이 있으며, 스턱스 넷의 경우 초기 개발단계부터 악성코드가 유입된 사례
44+
- [KISA > 정보보호 용어 > 공급망 사슬 공격](https://www.krcert.or.kr/kr/bbs/list.do?bbsId=B0001020&menuNo=205025&pageIndex=1&searchCnd=1&searchWrd=supply%20chain%20attack)
45+
- 제 3자 공격이라고도 불리며 주요 벤더사의 종속성에 침투하여 클라이언트의 시스템과 정보에 접근한다
46+
- [공급망 공격이란? | Cloudflare](https://www.cloudflare.com/ko-kr/learning/security/glossary/supply-chain-attack/)
47+
48+
## 메인테이너 계정 탈취 과정
49+
50+
![maintainer fishing](maintainer-fishing.webp 'l')
51+
52+
1. 메인테이너들에게 자격증명을 요청하는 이메일이 발송 됨
53+
- 이메일은 support@npmjs.org 주소를 위조
54+
2. 링크를 누르면 `npnjs`(npmjs가 아님)로 이동
55+
- 이러한 오타 피싱을 타이포스쿼팅 공격(Typosquating Attack)이라 함
56+
- 피싱 사이트에 <u>토큰이 쿼리로</u> 되어 있는데 이는 특정 메인테이너를 추적하기 위함
57+
3. 메인테이너는 피싱 사이트를 npmjs.org 로 오인해 로그인 진행
58+
59+
![automation token](automation-token.webp 'size=l;fit=true')
60+
61+
&nbsp;4. 계정 탈취 및 토큰 생성 완료
62+
63+
- `automation` 타입의 토큰은 `npm publish` 사용시 **2FA 정책을 사용하지 않는다.**
64+
65+
BOOM!
66+
67+
## 공격 코드 분석
68+
69+
그렇다면 악성 코드는 어떤 형태일까? 이번에는 공격 코드를 분석해보자.
70+
71+
코드 전문은 https://socket.dev/npm/package/is/files/5.0.0/index.js 에서 확인 가능하다.
72+
73+
### 1. 패키지 설치와 postinstall
74+
75+
```json
76+
// is/package.json
77+
{
78+
"name": "is",
79+
"version": "5.0.0",
80+
"scripts": {
81+
"postinstall": "node index.js"
82+
// ...
83+
}
84+
```
85+
86+
1. 사용자가 `npm i is`를 하게 되면
87+
2. `postinstall`이 실행되면서
88+
3. 해킹 코드가 있는 `index.js` 파일을 실행하게 된다.
89+
90+
### 2. 동적 함수 선언과 실행
91+
92+
```js
93+
// index.js
94+
Function(
95+
'of‌',
96+
`var throw‌‌‌‌‌‌‌‌‌‌‌‌‌,do‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,public‌‌‌‌‌‌‌‌‌‌‌‌,case‌‌‌‌‌‌‌‌‌‌‌‌‌‌,new‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,void‌‌‌‌‌‌‌‌‌‌‌‌‌‌,function‌‌‌‌‌‌‌‌‌‌,of‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,var‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,in‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌;function for‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌(throw‌‌‌‌‌‌‌‌‌‌‌‌‌,do‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,public‌‌‌‌‌‌‌‌‌‌‌‌){for(public‌‌‌‌‌‌‌‌‌‌‌‌=0x0;public‌‌‌‌‌‌‌‌‌‌‌‌<do‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌;public‌‌‌‌‌‌‌‌‌‌‌‌++)throw‌‌‌‌‌‌‌‌‌‌‌‌‌.push(throw‌‌‌‌‌‌‌‌‌‌‌‌‌.shift());return throw‌‌‌‌‌‌‌‌‌‌‌‌‌}
97+
const arguments‌‌‌‌‌‌‌‌‌=["\\u006c\\u0065\\u006e\\
98+
...
99+
arguments‌‌‌‌‌‌‌‌‌‌[new‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌(arguments‌‌‌‌‌‌‌‌‌[0x89])]||arguments‌‌‌‌‌‌‌‌‌‌)}
100+
})();`, // 약 8000 줄
101+
)({
102+
get void‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌() {
103+
return window;
104+
},
105+
get switch‌‌‌‌‌‌‌‌‌‌‌‌‌() {
106+
return require;
107+
},
108+
});
109+
```
110+
111+
악성 코드의 주요 흐름은 아래와 같다.
112+
113+
1. 함수 생성자를 활용하여 동적 함수 생성 및 호출
114+
- 메모리 단계에서 사라지도록 유도
115+
2. 인자로 넘긴 객체 `{ get void() { return window; }, … }``of`라는 인자로 넘김
116+
3. 본문의 8000줄의 코드에서 `of.void() // window` 반환 등으로 유저의 전역 객체에 접근해 정보를 캔다.
117+
- 이를 통해 정적 분석을 회피한다
118+
- `window`에 직접 접근하는 코드는 보안 모니터링에 걸리기 때문
119+
120+
<Callout className="mt-4" info>
121+
122+
동적 함수는 런타임에 함수를 생성하는 함수다.
123+
124+
아래와 같이 함수 생성자로 함수 생성 및 즉시 실행이 가능하다.
125+
126+
<code className="p-2">Function("a", "b", "return a + b")(1, 2) // 3</code>
127+
128+
`eval` 과 같이 스크립트를 "문자열"로 처리(실행)할 수 있기 때문에 위험한 코드다.
129+
130+
</Callout>
131+
132+
### 3. 예약어 기만
133+
134+
```js
135+
// Function 함수의 두 번째 인자. 문자열로 되어 있음.
136+
var throw‌‌‌‌‌‌‌‌‌‌‌‌‌, // 실제로는 throw + "보이지 않는 문자"들로 되어 있음.
137+
do‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,
138+
public‌‌‌‌‌‌‌‌‌‌‌‌,
139+
case‌‌‌‌‌‌‌‌‌‌‌‌‌‌,
140+
new‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,
141+
void‌‌‌‌‌‌‌‌‌‌‌‌‌‌,
142+
function‌‌‌‌‌‌‌‌‌‌,
143+
of‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,
144+
var‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌,
145+
in‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌;
146+
// ...
147+
const arguments‌‌‌‌‌‌‌‌‌ = [
148+
'\\u006c\\u0065\\u006e\\u0067\\u0074\\u0068', // length
149+
0x1,
150+
'\\u0062',
151+
0x0,
152+
'\\u0068',
153+
0x3,
154+
```
155+
156+
`throw` 같이 자주 사용되는 예약어에 <u>보이지 않는 문자</u>를 추가해 난독화 함으로써 유저가 쉽게 파악할 수 없도록 한다.
157+
158+
<Callout className="mt-4" info>
159+
160+
기본적으로 해커의 문자열 코드에는 모든 값에 "보이지 않는 문자"가 추가되어 있다.
161+
162+
![200c unicode](200c.webp 'size=l;fit=true;description=? private( 검색이 되지 않는다')
163+
164+
![200c unicode example](200c-1.webp 'size=s;fit=true')
165+
166+
그 이유는 `\u200C` 유니코드로 조합했기 때문이다.
167+
168+
<div className="flex flex-row">
169+
<p className="text-red-600 font-bold inline">
170+
값이 존재하지만 표시되지 않는 특징
171+
</p>
172+
을 활용해 검색되지 않도록 했다.
173+
</div>
174+
175+
![200c unicode example 2(private length)](200c-2.webp 'size=ss;fit=true;description=위(해커 파일) / 아래(정상)')
176+
177+
</Callout>
178+
179+
#### 예약어를 위와 같이 덮어쓰는건 몇가지 공격자 이점이 있다
180+
181+
- 일반 개발자들은 번들된 파일의 위험을 전혀 예측하지 못함
182+
- 코드 추적이 어려워져 보안패치까지 시간이 걸림
183+
- 진짜 예약어인지 덮어쓰여진 변수인지 구분이 어려움(private 경우 처럼)
184+
- 동일한 `private` 처럼 보여도 `p\u200Crivate`, `pri\u200Cvate` 처럼 무수히 많은 가지수가 생김
185+
- 전형적인 난독화 기법
186+
- 민감한 예약어(throw, require, …)가 없는 척 하면서 런타임 단계에서 수정 실행 하는 위험이 있다.
187+
188+
### 4. 전역 객체 접근 함수 선언
189+
190+
![global object](global-object.webp 'bold;description=약 8000줄 사이에 숨겨져 있는 코드들')
191+
192+
<br />
193+
194+
<Callout error>
195+
196+
전역 객체 호출이 왜 위험한가?
197+
198+
```js
199+
// 이제 다음에 접근 가능
200+
globalObj.process.env; // 환경변수 전체
201+
globalObj.process.cwd(); // 현재 디렉토리
202+
globalObj.require; // require 함수
203+
204+
// 가능한 예상 수집 대상 정보들
205+
const payload = {
206+
env: {
207+
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
208+
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
209+
DATABASE_URL: process.env.DATABASE_URL,
210+
// ... 모든 환경변수
211+
},
212+
system: {
213+
hostname: os.hostname(),
214+
username: os.userInfo().username,
215+
cwd: process.cwd(),
216+
platform: process.platform,
217+
arch: process.arch,
218+
},
219+
project: {
220+
packageJson: require('./package.json'),
221+
dependencies: /* 의존성 정보 */,
222+
scripts: /_ npm scripts \_/,
223+
}
224+
};
225+
226+
```
227+
228+
</Callout>
229+
230+
### 5. 웹 소켓 호출
231+
232+
![socket call](socket.webp 'l')
233+
234+
소켓을 연결하는 코드가 있는 것을 확인할 수 있다.
235+
236+
#### 소켓이 연결 되면 아래와 같은 문제가 생긴다
237+
238+
1. 데이터 전송과정을 거치며 유저의 데이터를 탈취한다.
239+
2. 위에서 "전역 객체"에 접근하는 코드를 확인했다. 이 값과 소켓 연결을 통해 유저의 컴퓨터를 제어할 수 있다.
240+
3. 모든 메시지가 실행 가능한 JavaScript로 처리되므로 해커에게 각종 명령어를 제공하게 된다.
241+
242+
(대충 멸망)
243+
244+
## 방어 방법
245+
246+
- 종속성 버전을 pin 하여 고정 버전으로 사용한다.
247+
- [Renovate](/posts/renovate) 등의 도구로 자동으로 버전 업데이트하는 것을 추천한다.
248+
- ci 명령어 혹은 production 인스톨을 한다.
249+
- `postinstall` 스크립트는 `npm ci` 명령어나 `npm install --production` 명령어에서는 실행되지 않는다
250+
251+
## 시사점
252+
253+
### 1. 의존성 하나가 전체 앱의 보안에 영향을 줄 수 있다
254+
255+
- 프론트엔드 개발 환경은 빌드 툴(Vite, Webpack), 유틸 라이브러리, 폴리필 등 수많은 npm 패키지를 사용함
256+
- 작은 유틸 하나(is)도 감염되면 전체 앱이 영향 받음
257+
258+
### 2. 계정 탈취는 생각보다 흔하다
259+
260+
- 이 사례처럼 오래된 메인테이너의 계정이 공격받는 일이 종종 발생
261+
- 개인 개발자가 관리하던 패키지도 공격자에 의해 감염 될 수 있음
262+
263+
### 3. 잠재적 위험 탐지 도구 사용
264+
265+
![audit](audit.webp)
266+
267+
`npm audit`, `pnpm audit` 등으로 의존성의 리스크를 점검
268+
269+
[Renovate](/posts/renovate)를 적용하여 주기적 업데이트 적용
270+
271+
## 마치며
272+
273+
"특정 패키지가 공격당했다"고 자주 들었지만 실제로 어떤 악성 코드가 있는지는 잘 몰라 항상 궁금했다.
274+
275+
이번 기회에 악성 코드를 분석하고 방어 방법을 고민해보게 되어 의미있는 시간이었다.
276+
277+
디펜던시 버전을 고정하여 관리하고 Renovate 등의 도구로 안전하게 버전 관리 하자.
278+
279+
## 참고
280+
281+
- https://socket.dev/blog/npm-phishing-email-targets-developers-with-typosquatted-domain
282+
- https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised
53.6 KB
Loading
30.8 KB
Loading
76 KB
Loading

0 commit comments

Comments
 (0)