OWASP 카테고리: MASVS-NETWORK: 네트워크 커뮤니케이션
개요
DownloadManager는 API 수준 9에 도입된 시스템 서비스입니다. 오래 실행되는 HTTP 다운로드를 처리하고 애플리케이션이 백그라운드 작업으로 파일을 다운로드할 수 있도록 합니다. API는 HTTP 상호작용을 처리하고 다운로드가 실패되거나 연결이 변경되거나 시스템이 재부팅된 후에 다운로드를 다시 시도합니다.
DownloadManager에는 Android 애플리케이션에서 다운로드를 관리하는 데 안전하지 않은 선택이 되도록 하는 보안 관련 약점이 있습니다.
(1) 다운로드 제공자의 CVE
2018년 다운로드 제공자에서 세 개의 CVE가 발견되어 패치가 적용되었습니다. 각각의 요약은 다음과 같습니다 (기술 세부정보 참고).
- 다운로드 제공자 권한 우회 - 권한이 부여되지 않은 악성 앱이 다운로드 제공자에서 모든 항목을 가져올 수 있습니다. 여기에는 파일 이름, 설명, 제목, 경로, URL과 같은 민감한 정보와 다운로드된 모든 파일에 대한 전체 읽기/쓰기 권한이 포함될 수 있습니다. 악성 앱은 백그라운드에서 실행되어 모든 다운로드를 모니터링하고 콘텐츠를 원격으로 유출하거나, 합법적인 요청자가 액세스하기 전에 파일을 즉시 수정할 수 있습니다. 이로 인해 업데이트를 다운로드할 수 없는 등 핵심 애플리케이션에 대한 서비스 거부가 발생할 수 있습니다.
- 다운로드 제공자 SQL 삽입 – SQL 삽입 취약점을 통해 권한이 없는 악성 애플리케이션이 다운로드 제공자의 모든 항목을 가져올 수 있습니다. 또한
android.permission.INTERNET와 같이 권한이 제한된 애플리케이션도 다른 URI에서 모든 데이터베이스 콘텐츠에 액세스할 수 있습니다. 파일 이름, 설명, 제목, 경로, URL과 같은 민감한 정보가 검색될 수 있으며, 권한에 따라 다운로드된 콘텐츠에 액세스할 수도 있습니다. - 다운로드 제공자 요청 헤더 정보 공개 -
android.permission.INTERNET권한이 부여된 악성 애플리케이션이 다운로드 제공자 요청 헤더 테이블에서 모든 항목을 가져올 수 있습니다. 이러한 헤더에는 Android 브라우저 또는 Google Chrome을 비롯한 애플리케이션에서 시작된 다운로드의 세션 쿠키나 인증 헤더와 같은 민감한 정보가 포함될 수 있습니다. 이렇게 하면 공격자가 민감한 사용자 데이터를 획득한 플랫폼에서 사용자를 가장할 수 있습니다.
(2) 위험한 권한
API 수준이 29보다 낮은 DownloadManager에는 위험한 권한(android.permission.WRITE_EXTERNAL_STORAGE)이 필요합니다. API 수준 29 이상에서는 android.permission.WRITE_EXTERNAL_STORAGE 권한이 필요하지 않지만 URI는 애플리케이션이 소유한 디렉터리 내 경로 또는 최상위 '다운로드' 디렉터리 내 경로를 참조해야 합니다.
(3) Uri.parse()에 대한 의존도
DownloadManager는 Uri.parse() 메서드를 사용하여 요청된 다운로드의 위치를 파싱합니다. 성능을 위해 Uri 클래스는 신뢰할 수 없는 입력에 유효성 검사를 거의 적용하지 않습니다.
영향
DownloadManager를 사용하면 외부 저장소에 대한 쓰기 권한을 악용하여 취약점이 발생할 수 있습니다. android.permission.WRITE_EXTERNAL_STORAGE 권한은 외부 저장소에 대한 광범위한 액세스를 허용하므로 공격자가 파일을 무음으로 수정하고 다운로드하거나, 잠재적으로 악성 앱을 설치하거나, 핵심 앱에 대한 서비스를 거부하거나, 앱이 비정상 종료되도록 할 수 있습니다. 악의적인 행위자는 Uri.parse()로 전송되는 내용을 조작하여 사용자가 유해한 파일을 다운로드하도록 할 수도 있습니다.
완화 조치
DownloadManager를 사용하는 대신 HTTP 클라이언트 (예: Cronet), 프로세스 스케줄러/관리자, 네트워크 손실 시 재시도를 보장하는 방법을 사용하여 앱에서 직접 다운로드를 설정합니다. 라이브러리 문서에는 샘플 앱으로 연결되는 링크와 구현 방법에 관한 안내가 포함되어 있습니다.
애플리케이션에 프로세스 예약 관리, 백그라운드에서 다운로드 실행, 네트워크 손실 후 다운로드 재시도 기능이 필요한 경우 WorkManager 및 ForegroundServices를 포함하는 것이 좋습니다.
Cronet을 사용하여 다운로드를 설정하는 예시 코드는 Cronet Codelab에서 가져온 다음 코드와 같습니다.
Kotlin
override suspend fun downloadImage(url: String): ImageDownloaderResult {
val startNanoTime = System.nanoTime()
return suspendCoroutine {
cont ->
val request = engine.newUrlRequestBuilder(url, object: ReadToMemoryCronetCallback() {
override fun onSucceeded(
request: UrlRequest,
info: UrlResponseInfo,
bodyBytes: ByteArray) {
cont.resume(ImageDownloaderResult(
successful = true,
blob = bodyBytes,
latency = Duration.ofNanos(System.nanoTime() - startNanoTime),
wasCached = info.wasCached(),
downloaderRef = this@CronetImageDownloader))
}
override fun onFailed(
request: UrlRequest,
info: UrlResponseInfo,
error: CronetException
) {
Log.w(LOGGER_TAG, "Cronet download failed!", error)
cont.resume(ImageDownloaderResult(
successful = false,
blob = ByteArray(0),
latency = Duration.ZERO,
wasCached = info.wasCached(),
downloaderRef = this@CronetImageDownloader))
}
}, executor)
request.build().start()
}
}
Java
@Override
public CompletableFuture<ImageDownloaderResult> downloadImage(String url) {
long startNanoTime = System.nanoTime();
return CompletableFuture.supplyAsync(() -> {
UrlRequest.Builder requestBuilder = engine.newUrlRequestBuilder(url, new ReadToMemoryCronetCallback() {
@Override
public void onSucceeded(UrlRequest request, UrlResponseInfo info, byte[] bodyBytes) {
return ImageDownloaderResult.builder()
.successful(true)
.blob(bodyBytes)
.latency(Duration.ofNanos(System.nanoTime() - startNanoTime))
.wasCached(info.wasCached())
.downloaderRef(CronetImageDownloader.this)
.build();
}
@Override
public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {
Log.w(LOGGER_TAG, "Cronet download failed!", error);
return ImageDownloaderResult.builder()
.successful(false)
.blob(new byte[0])
.latency(Duration.ZERO)
.wasCached(info.wasCached())
.downloaderRef(CronetImageDownloader.this)
.build();
}
}, executor);
UrlRequest urlRequest = requestBuilder.build();
urlRequest.start();
return urlRequest.getResult();
});
}
리소스
- DownloadManager 기본 문서 페이지
- DownloadManager CVE 보고서
- Android 권한 우회 CVE 2018-9468
- Android 다운로드 제공자 SQL 삽입 CVE-2018-9493
- Android 다운로드 제공자 권한 우회 CVE2018-9468
- Cronet 기본 문서 페이지
- 애플리케이션에서 Cronet 사용 안내
- 샘플 Cronet 구현
- URI 문서
- ForegroundService 문서
- WorkManager 문서