简单分析下CVE-2019-6447

0x01 漏洞描述

ES File Explorer File Manager application for Android(ES文件浏览器或文件管理器)是一款基于Android系统的多功能手机文件、程序和进程管理器,它支持在手机、电脑、远程和蓝牙间浏览管理文件。 基于Android平台的ES File Explorer File Manager application 4.1.9.7.4及之前版本中存在安全漏洞,该漏洞源于ES应用程序在运行一次之后(CVE-2019-6447),该端口并未关闭依旧可以通过HTTP协议接收JSON数据。攻击者可通过向TCP 59777端口发送请求利用该漏洞读取任意文件或执行应用程序。

0x02 复现

POC

Github地址:https://github.com/fs0c131y/ESFileExplorerOpenPortVuln

步骤

  1. 安装ES文件管理器,版本低于4.1.9.7.4并启动,不执行任何操作

  2. 接入与电脑同一局域网

  3. 对指定设备进行扫描
    nmap -sS -p 1-65535 -v 192.168.2.63

  4. 验证POC
    执行命令python3 poc.py --cmd getDeviceInfo --network 192.168.2.

回显设备信息和FTP端口号
复现成功

漏洞分析

从一些渠道下载到了低于官网的软件安装包,然后解包后将3个classes.dex转为jar
最终目录图如下

用JD-GUI打开JAR包

对POC中的关键字如59777command等进行全局搜索,最终取得执行漏洞的关键代码和,具体如下

漏洞包关键代码

开启服务器相关代码

com.estrongs.android.f.a

public static boolean a(boolean paramBoolean)
{
boolean bool1 = true;
synchronized (g)
{
if ((f != null) && (f.d())) {
return true;
}
a locala = f;
if (locala == null) {}
}
try
{
f.i();
f = null;
int i = 0;
for (;;)
{
if (i < 5) {
try
{
f = new a("/sdcard", 59777 + i, paramBoolean);
int j = 1000;
while (j > 0)
{
boolean bool2 = f.d();
if (bool2)
{
paramBoolean = bool1;
return paramBoolean;
localObject2 = finally;
throw localObject2;
}
Thread.sleep(200L);
j -= 200;
}
}
catch (Exception localException1)
{
f = null;
localException1.printStackTrace();
i += 1;
}
}
}
return false;
}
catch (Exception localException2)
{
for (;;)
{
continue;
paramBoolean = false;
}
}
}

其中a继承自c。

com.estrongs.android.f.c

public c(int paramInt)
{
this.b = paramInt;
this.l = new ServerSocket(this.b);
this.c = new Thread(new Runnable()
{
public void run()
{
try
{
while (!c.a(c.this)) {
new c.a(c.this, c.b(c.this).accept());
}
return;
}
catch (IOException localIOException) {}
}
});
this.c.setDaemon(true);
this.c.start();
}

com.estrongs.android.f.a

public c.b a(String paramString1, String paramString2, Properties paramProperties1, Properties paramProperties2, Properties paramProperties3)
{
if (paramString1.startsWith("/estrongs_filemgr_oauth_result"))
{
paramString1 = CreateOAuthNetDisk.b();
if (paramString1 != null) {
paramString1.a(paramProperties2);
}
return null;
}
if (paramString2.equals("POST"))//判断发送方法是否为POST
{
localObject = new String(g());
try
{
localObject = new JSONObject((String)localObject);//JSONG解码
String str = ((JSONObject)localObject).getString("command");//获取JSON中command字段值
if (str.equals("listFiles")) {//获取文件列表
return b(paramString1);
}
if (str.equals("listPics")) {//获取图片列表
return d();
}
if (str.equals("listVideos")) {//获取视频列表
return e();
}
if (str.equals("listAudios")) {//获取音频列表
return f();
}
if (str.equals("listApps")) {//获取软件列表
return a(0);
}
if (str.equals("listAppsSystem")) {//获取系统软件列表
return a(1);
}
if (str.equals("listAppsPhone")) {//获取电话软件列表
return a(2);
}
if (str.equals("listAppsSdcard")) {//获取安装在SD卡中的软件列表
return a(3);
}
if (str.equals("listAppsAll")) {//获取全部APP
return a(4);
}
if (str.equals("getAppThumbnail")) {//获取APP缩略图
return d((JSONObject)localObject);
}
if (str.equals("appLaunch")) {//启动APP
return a((JSONObject)localObject);
}
if (str.equals("appPull")) {//下载APP
return c((JSONObject)localObject);
}
if (str.equals("getDeviceInfo"))//获取设备信息
{
paramString1 = b((JSONObject)localObject);
return paramString1;
}
}
catch (JSONException paramString1)
{
paramString1.printStackTrace();
return new c.b(this, "500 Internal Server Error", "text/plain", paramString1.toString());
}
}
Object localObject = ah.bL(paramString1);
if ((localObject == null) || (ah.I((String)localObject) == 0))
{
if (localObject == null) {
return super.a(paramString1, paramString2, paramProperties1, paramProperties2, paramProperties3);
}
return super.a((String)localObject, paramString2, paramProperties1, paramProperties2, paramProperties3);
}
paramString1 = paramProperties1.getProperty("range");
if ((paramString1 != null) && (paramString1.startsWith("bytes=")))
{
paramString2 = paramString1.substring("bytes=".length());
int i = paramString2.indexOf('-');
paramString1 = paramString2;
if (i > 0) {
paramString1 = paramString2.substring(0, i);
}
}
for (;;)
{
try
{
l = Long.parseLong(paramString1);
return a((String)localObject, l, 0L);
}
catch (NumberFormatException paramString1)
{
l = 0L;
continue;
}
long l = 0L;
}
}

由于漏洞应用条件是在同一个局域网下,所以应用在获取完传入的方法【指令】后调用com.estrongs.android.util.af中的bM方法验证URI地址

public static String bM(String paramString)
{
int i3 = 1;
if (paramString == null) {
return null;
}
String str1 = ad.a();
int i2;
if ((paramString.startsWith("http://127.0.0.1")) || (paramString.startsWith("http://" + str1)))
{
i1 = paramString.indexOf("/", 7);
str1 = paramString;
if (i1 >= 0) {
str1 = paramString.substring(i1);
}
if (!str1.startsWith("/")) {
break label116;
}
i2 = str1.indexOf('/', 1);
}

并且同时调用comestrongsandroidutilad中的a方法检测当前环境是否处于WIFI环境下

public class ad
{
public static String a()
{
Object localObject = FexApplication.a();
if (b())
{
localObject = ((WifiManager)((Context)localObject).getSystemService("wifi")).getConnectionInfo();
if ((localObject == null) || (((WifiInfo)localObject).getIpAddress() == 0)) {
return o.a();
}
int i = ((WifiInfo)localObject).getIpAddress();
return (i & 0xFF) + "." + (i >> 8 & 0xFF) + "." + (i >> 16 & 0xFF) + "." + (i >> 24 & 0xFF);
}
return o.a();
}

当传入的commandlistFiles时,会调用b()函数获取文件信息并以Json格式返回,相关代码如下

public c.b b(String paramString)
{
if (ah.I(paramString) == 0) {}
for (;;)
{
int i;
try
{
Object localObject = new File(paramString).listFiles();
DateFormat localDateFormat = com.estrongs.android.pop.h.a().K();
SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat(" hh:mm:ss a");
paramString = "[\r\n";
i = 0;
if (i < localObject.length)
{
paramString = paramString + "{";
paramString = paramString + "\"name\":\"" + localObject.getName() + "\", ";//文件名
String str = paramString + "\"time\":\"" + localDateFormat.format(new Date(localObject.lastModified())) + localSimpleDateFormat.format(new Date(localObject.lastModified())) + "\", ";//时间
if (!localObject.isDirectory()) {
break label437;
}
paramString = "folder";
paramString = str + "\"type\":\"" + paramString + "\", ";//文件类型
paramString = paramString + "\"size\":\"" + a(localObject.length()) + "\", ";//大小
if (i == localObject.length - 1)
{
paramString = paramString + "}\r\n";
break label430;
}
paramString = paramString + "}, \r\n";
break label430;
}
paramString = paramString + "]";
localObject = new c.b(this, "200 OK", "text/plain", a(paramString));
((c.b)localObject).a("Content-Length", "" + paramString.getBytes("utf-8").length);
return localObject;
}
catch (Exception paramString)//错误处理
{
paramString.printStackTrace();
return new c.b(this, "500 Internal Server Error", "text/plain", paramString.toString());
}
return new c.b(this, "400 Bad Request", "text/plain", "Not Supported");
label430:
i += 1;
continue;
label437:
paramString = "file";
}
}

当获取的commandappLaunch时,会调用a()方法执行APP,运行成功会返回HTTP CODE 200,不存在则返回500并返回not found the package 信息

public c.b a(JSONObject paramJSONObject)
{
for (;;)
{
try
{
paramJSONObject = paramJSONObject.getString("appPackageName");
if (paramJSONObject != null)
{
new Intent();
paramJSONObject = FexApplication.c().getPackageManager().getLaunchIntentForPackage(paramJSONObject);
i = 0;
if (paramJSONObject != null)
{
FexApplication.c().startActivity(paramJSONObject);
paramJSONObject = "{" + "\"result\":\"" + i + "\"";
paramJSONObject = paramJSONObject + "}";
c.b localb = new c.b(this, "200 OK", "text/plain", a(paramJSONObject));
localb.a("Content-Length", "" + paramJSONObject.getBytes("utf-8").length);
return localb;
}
}
else
{
paramJSONObject = new c.b(this, "500 Internal Server Error", "text/plain", "not found the package " + paramJSONObject);
return paramJSONObject;
}
}
catch (Exception paramJSONObject)
{
paramJSONObject.printStackTrace();
return new c.b(this, "500 Internal Server Error", "text/plain", paramJSONObject.toString());
}
int i = -1;
}

当获取到的commandappPull时,会调用C方法下载一个指定文件,如果存在则返回200并且以数据流传输回设备中,否则返回500并回显not found the package,代码如下

public c.b c(JSONObject paramJSONObject)
{
try
{
Object localObject = paramJSONObject.getString("appPackageName");
if (localObject != null)
{
paramJSONObject = new File(FexApplication.c().getPackageManager().getApplicationInfo((String)localObject, 0).sourceDir);
if (paramJSONObject.exists())
{
localObject = new c.b(this, "200 OK", "application/octet-stream", new FileInputStream(paramJSONObject));
((c.b)localObject).a("Content-Length", "" + paramJSONObject.length());
return localObject;
}
}
paramJSONObject = new c.b(this, "500 Internal Server Error", "text/plain", "not found the package " + (String)localObject);
return paramJSONObject;
}
catch (Exception paramJSONObject)
{
paramJSONObject.printStackTrace();
}
return new c.b(this, "500 Internal Server Error", "text/plain", paramJSONObject.toString());
}

当传入的commandlistAppslistAppsSystemlistAppsPhonelistAppsSdcardlistAppsAll时,会调用a(int paramInt)方法执行相应功能,a方法代码如下

public c.b a(int paramInt)
{
Object localObject2;
PackageInfo localPackageInfo;
Object localObject3;
for (;;)
{
try
{
Object localObject1 = FexApplication.a().b(8192);
localObject2 = new LinkedList();
localObject1 = ((List)localObject1).iterator();
if (!((Iterator)localObject1).hasNext()) {
break;
}
localPackageInfo = (PackageInfo)((Iterator)localObject1).next();
localObject3 = localPackageInfo.applicationInfo;
if (paramInt == 0)//传入a的等于0
{
if (((((ApplicationInfo)localObject3).flags & 0x80) == 0) && ((((ApplicationInfo)localObject3).flags & 0x1) != 0)) {
continue;
}
((LinkedList)localObject2).add(localPackageInfo);
continue;
}
if (paramInt != 1) {
break label146;
}
}
catch (Exception localException)
{
localException.printStackTrace();
return new c.b(this, "500 Internal Server Error", "text/plain", localException.toString());
}
if ((((ApplicationInfo)localObject3).flags & 0x1) > 0)
{
((LinkedList)localObject2).add(localPackageInfo);
continue;
label146:
if (paramInt == 2)//传入a的值等于2
{
if ((((ApplicationInfo)localObject3).flags & 0x40000) == 0) {
((LinkedList)localObject2).add(localPackageInfo);
}
}
else if (paramInt == 3)//传入a的值等于3
{
if ((((ApplicationInfo)localObject3).flags & 0x40000) != 0) {
((LinkedList)localObject2).add(localPackageInfo);
}
}
else if (paramInt == 4) {//传入a的等于4
((LinkedList)localObject2).add(localPackageInfo);
}
}
}
for (;;)
{
String str;
if (paramInt < ((LinkedList)localObject2).size())
{
localPackageInfo = (PackageInfo)((LinkedList)localObject2).get(paramInt);
localObject3 = new File(localPackageInfo.applicationInfo.sourceDir);
if (!((File)localObject3).exists()) {}
for (long l = 0L;; l = ((File)localObject3).length())
{
//回显JSON的字符串进行拼接
str = localException + "{";
str = str + "\"packageName\":\"" + localPackageInfo.packageName + "\", ";
str = str + "\"label\":\"" + com.estrongs.android.pop.utils.c.a(FexApplication.a().getPackageManager(), localPackageInfo.applicationInfo) + "\", ";
str = str + "\"version\":\"" + localPackageInfo.versionName + "\", ";
str = str + "\"versionCode\":\"" + localPackageInfo.versionCode + "\", ";
localObject3 = localPackageInfo.applicationInfo.sourceDir;
str = str + "\"location\":\"" + (String)localObject3 + "\", ";
str = str + "\"size\":\"" + l + "\", ";
str = str + "\"status\":\"" + localPackageInfo.applicationInfo.backupAgentName + "\", ";
str = str + "\"mTime\":\"" + localPackageInfo.lastUpdateTime + "\"";
if (paramInt >= ((LinkedList)localObject2).size() - 1) {
break;
}
str = str + "},\r\n";
break label760;
}
str = str + "}\r\n";
}
else
{
str = str + "]";
localObject2 = new c.b(this, "200 OK", "text/plain", a(str));
((c.b)localObject2).a("Content-Length", "" + str.getBytes("utf-8").length);
return localObject2;
str = "[\r\n";
paramInt = 0;
continue;
}
label760:
paramInt += 1;
}
}

总结

漏洞主要成因是官方没有对文件分享功能的传入功能的传入者进行身份合法性验证,导致可以被任意执行命令,第一次做APP分析,不足之处请多多指出