[하루한줄] CVE-2025-11001, CVE-2025-11002: 7-Zip의 심볼릭링크 처리 취약점으로 인한 Arbitrary File Write / RCE 가능성

URL

https://www.zerodayinitiative.com/advisories/ZDI-25-949/

Target

  • 7-Zip : ≤ 24.09

Explain

CVE-2025-11001 / CVE-2025-11002는 7-Zip의 ZIP 추출 과정에서 심볼릭 링크(특히 Linux/WSL 방식으로 표현된 링크 데이터)를 파싱·처리하는 로직의 논리적 결함으로 인해 발생합니다. 파서가 Windows 절대경로C:\...를 포함한 링크를 리눅스식 검사로 절대 여부를 오판하면 isRelative가 잘못 설정되고, 이후 ‘relativize’ 과정에서 ZIP 내부 디렉터리 접두사가 붙으면서 IsSafePath 검사로 우회가 가능해집니다. 추가로 일부 유효성 검사 흐름은 항목 타입 검사(예: _item.IsDir)로 인해 빠져나가 버려 최종적으로 시스템에 reparse 데이터가 설정되어 심볼릭 링크가 생성되고, 추출 중에 심볼릭 링크를 따라 임의 위치에 파일을 쓰게 됩니다.

IsSafePath와 CLinkLevelsInfo::Parse diff

-bool IsSafePath(const UString &path)
+static bool IsSafePath(const UString &path, bool isWSL)
 {
   CLinkLevelsInfo levelsInfo;
-  levelsInfo.Parse(path);
+  levelsInfo.Parse(path, isWSL);
   return !levelsInfo.IsAbsolute
       && levelsInfo.LowLevel >= 0
       && levelsInfo.FinalLevel > 0;
 }
 
+bool IsSafePath(const UString &path);
+bool IsSafePath(const UString &path)
+{
+  return IsSafePath(path, false); // isWSL
+}

+void CLinkLevelsInfo::Parse(const UString &path, bool isWSL)
 {
-  IsAbsolute = NName::IsAbsolutePath(path);
-
+  IsAbsolute = isWSL ?
+      IS_PATH_SEPAR(path[0]) :
+      NName::IsAbsolutePath(path);
   LowLevel = 0;
   FinalLevel = 0;
 }

7-Zip 24.09와 25.00을 diff 해보면 이번 릴리스에서 몇 가지 버그가 수정된 것을 볼 수 있습니다. 이번 패치는 CPP/7zip/UI/Common/ArchiveExtractCallback.cpp의 zip 압축 해제 관련 심볼릭링크 코드가 많이 수정되었습니다. 그 중에서도 IsSafePath 함수가 눈에 띄게 패치되었습니다.

Root Cause

1) 심볼릭링크 식별

if (_curSize_Defined && _curSize > 0 && _curSize < (1 << 12))
{
  if (_fi.IsLinuxSymLink())
  {
    is_SymLink_in_Data = true;
    _is_SymLink_in_Data_Linux = true;
  }
  else if (_fi.IsReparse())
  {
    is_SymLink_in_Data = true;
    _is_SymLink_in_Data_Linux = false;
  }
}

시작점은 CArchiveExtractCallback::GetStream()이며, 이 함수는 ReadLink를 호출합니다. GetStream()CArchiveExtractCallback::GetExtractStream()을 호출하는데, 파일 크기가 작으면 심볼릭링크인지 여부를 식별합니다. 그 이후 전체 파일 검사를 수행합니다.

2) reparse 모드 및 링크 파싱: CLinkInfo::Parse 호출

추가 처리 과정을 거친 뒤 CArchiveExtractCallback::CloseReparseAndFile()로 넘어가는데, 여기서 문제가 시작됩니다. 이 메서드는 링크를 파싱하여 그것이 가리키려는 위치(목적지)를 알아내려 시도합니다.

// 정의
bool CLinkInfo::Parse(const Byte *data, size_t dataSize, bool isLinuxData);
bool repraseMode = false;
bool needSetReparse = false;
CLinkInfo linkInfo;

if (_bufPtrSeqOutStream)
{
  repraseMode = true;
  reparseSize = _bufPtrSeqOutStream_Spec->GetPos();
  if (_curSize_Defined && reparseSize == _outMemBuf.Size())
  {
    // _is_SymLink_in_Data_Linux == true 
    needSetReparse = linkInfo.Parse(_outMemBuf, reparseSize, _is_SymLink_in_Data_Linux);
    if (!needSetReparse)
      res = SendMessageError_with_LastError("Incorrect reparse stream", us2fs(_item.Path));
  }
}

파서는 두 가지 중요한 속성을 설정합니다.

  • Link path (심볼릭링크의 목적지 경로)
  • isRelative (심볼릭링크가 상대 경로인지 여부)

Issue #1

리눅스 심볼릭링크가 윈도우 스타일의 C:\ 경로로 설정될 수 있었습니다.

#ifdef SUPPORT_LINKS
if (repraseMode)
{
  _curSize = reparseSize;
  _curSize_Defined = true;
  
  #ifdef SUPPORT_LINKS
  if (needSetReparse)
  {
    if (!DeleteFileAlways(_diskFilePath))
    {
      RINOK(SendMessageError_with_LastError("can't delete file", _diskFilePath))
    }
    {
      bool linkWasSet = false;
      RINOK(SetFromLinkPath(_diskFilePath, linkInfo, linkWasSet))
      if (linkWasSet)
        _isSymLinkCreated = linkInfo.IsSymLink();
      else
        _needSetAttrib = false;
    }

  }
  #endif
}
#endif

링크 경로는 전체 C:\ 경로로 설정되지만, 파서는 절대 경로 판단을 리눅스 스타일의 검사로 따르기 때문에 그 링크를 relative로 표기하게 됩니다.

Issue #2

SetFromLinkPath는 지정된 경로로 심볼릭링크를 생성하는 함수입니다. 다만 절대 경로로 링크를 만드는 것을 막는 안전 장치가 아래와 같이 존재했습니다.

if (linkInfo.isRelative)
  relatPath = GetDirPrefixOf(_item.Path);
relatPath += linkInfo.linkPath;

if (!IsSafePath(relatPath))
{
  return SendMessageError2(
        0, // errorCode
        "Dangerous link path was ignored",
        us2fs(_item.Path),
        us2fs(linkInfo.linkPath)); // us2fs(relatPath)
}

7-Zip은 새로 추출되는 zip 파일 내부에서 링크가 가리킬 상대 목적지 경로를 구성한 후 IsSafePath로 검사합니다. 상대 링크의 경우 심볼릭링크가 위치한 zip 내부 디렉터리 경로를 링크 경로 앞에 덧붙여 검사합니다.

Issue #3

Issue 1에서 확인했듯, 상대링크로 잘못 판단되어 isRelative == true 로 설정됩니다. 심볼릭링크가 zip의 루트가 아닌 아무 디렉터리 안에 있으면, 그 내부 경로가 링크 앞에 더해져서 검사 시 우회가 가능해집니다.

검사는 IsSafePath("some/directory/in/zip" + "C:\some\other\path")처럼 평가되며, 이 경우 true로 판정되어 통과됩니다.

Issue #4

실제 심볼릭링크를 생성하기 전에 링크 경로의 유효성을 확인하는 검사가 있어야 하지만, 그 이전에 주어진 ‘item(심볼릭링크)’이 디렉터리인지 여부를 확인하는 코드가 있었습니다.

if (!_ntOptions.SymLinks_AllowDangerous.Val)
{
  #ifdef _WIN32
  if (_item.IsDir) // NOPE
  #endif
  if (linkInfo.isRelative)
    {
      CLinkLevelsInfo levelsInfo;
      levelsInfo.Parse(linkInfo.linkPath);
      if (levelsInfo.FinalLevel < 1 || levelsInfo.IsAbsolute)
      {
        return SendMessageError2(
          0, // errorCode
          "Dangerous symbolic link path was ignored",
          us2fs(_item.Path),
          us2fs(linkInfo.linkPath));
      }
    }
}

이때, 항목은 디렉터리가 아니므로 통과하게 되어 사실상 유효성 검사가 우회됩니다.

Issue #5

위 4가지의 Issue를 모두 마치면 다음과 같이 심볼릭링크가 생성됩니다.

// existPath -> C:\some\other\path (symlink destination)
// data -> path for symlink to be created 
// Initializes reparse data for symlink creation
if (!FillLinkData(data, fs2us(existPath), !linkInfo.isJunction, linkInfo.isWSL))
  return SendMessageError("Cannot fill link data", us2fs(_item.Path));

/// ...

// creates symlink
if (!NFile::NIO::SetReparseData(fullProcessedPath, _item.IsDir, data, (DWORD)data.Size()))
{
  RINOK(SendMessageError_with_LastError(kCantCreateSymLink, fullProcessedPath))
  return S_OK;
}

PoC

data/link -> (symlink) C:\Users\YOURUSERNAME\Desktop (또는 원하는 다른 위치)
data/link -> (Directory)
data/link/calc.exe -> (추출 시 대상 디렉터리에 쓰고자 하는 파일)

심다음과 같은 디렉터리 구조를 만들어 압축하면 됩니다.

이 경우 link(심볼릭링크)가 먼저 풀려 생성되고, 이후 calc.exe가 symlink가 가리키는 실제 위치(예: C:\Users\YOURUSERNAME\Desktop)로 풀려 쓰여집니다. 7-Zip이 심볼릭링크를 따라가서 바이너리를 임의의 위치에 쓰게 되는 겁니다.

Reference

https://github.com/pacbypass/CVE-2025-11001



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.