导语:今年早些时候,我收到了我们市政府的一个NextDoor讯息,声称他们为民众提供了(购买)一个智慧城市应用程序。
今年早些时候,我收到了我们市政府的一个NextDoor讯息,声称他们为民众提供了(购买)一个智慧城市应用程序。在此之前并没有任何关于这个应用程序的信息,而我也没有在网上找到其相关的内容,所以对于它我非常有兴趣尽兴更多地了解。

根据应用程序的描述,Bright City是“为城市和执法机构面向民众提供服务的专用移动应用程序”。除了NextDoor公告中提到的“财产锁箱”功能外,该应用还支持接收和提交报告可疑活动,当民众离开家之后进行财产监控保护,保护公民的家,并可报告市政维护问题。Bright City的网站上还介绍了一些额外的功能,包括公民可以去了解市政费用的去向,许可证等内容。

而我们可以看到应用程序中涉及的信息的性质似乎非常敏感,所以我很有兴趣进行仔细观察。我下载安装了应用程序,创建了一个帐户并登录。这是初始面板:

接下来我开始浏览应用中的某些选项,以生成一些API流量,在这一过程中没有发现到一些非常严重的问题 。下面是应用程序在获取当前用户的配置文件信息时所做的请求示例:
GET https://api.brightcityapps.com/api/user/getuser/***REMOVED*** HTTP/1.1 Host: api.brightcityapps.com Connection: keep-alive Accept: application/json, text/javascript, */*; q=0.01 User-Agent: Mozilla/5.0 (Linux; Android 7.1.2; Pixel XL Build/NHG47L; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.83 Mobile Safari/537.36 Accept-Encoding: gzip, deflate Accept-Language: en-US X-Requested-With: com.mobilesciencetech.brightcity
这里和我最近写的另一个本地市政应用程序类似,该请求不需要任何身份验证。毫无疑问这是令人震惊的,因为这会使一切变得更糟。以下是对上述请求的API响应:
{
"BusinessWatches": [],
"Carrier": null,
"City": null,
"CityGroups": [],
"CityGroupPermissions": [],
"DepartmentCallDetails": [],
"EventSignUps": [],
"HomeWatches": [],
"Lockboxes": [],
"Lockboxes1": [],
"LockboxInsurances": [],
"Lookouts": [],
"LookoutInfoes": [],
"Maintenances": [],
"OpenRecords": [],
"PatrolRequests": [],
"Permissions": [],
"Photo": null,
"Supports": [],
"SurveyResponses": [],
"UserNotificationSettings": [],
"UserLogins": [],
"UsersDevices": [],
"UserTransactions": [],
"UtilityBills": [],
"id": ***REMOVED***,
"cityid": 17,
"first": "Randy",
"mi": null,
"last": "Westergren",
"phone": null,
"cell": "***REMOVED***",
"carrierid": null,
"email": "***REMOVED***",
"dob": null,
"username": "rwestergren",
"password": "***REMOVED***",
"thumb": null,
"created": "2017-05-21T16:57:00",
"active": 1,
"admin": 0,
"brainTreeCustomerId": null
}
请注意,虽然我从上面的输出中删除了它,但用户的密码是以纯文本形式返回的。显然,这一点也非常的糟糕。
接下来,我决定反编译应用程序,看看我是否可以找到更详尽的API端点列表(以及是否需要任何身份验证)。这是过程中主要的活动:
package com.mobilesciencetech.brightcity;
import android.content.Intent;
import android.os.Bundle;
import org.apache.cordova.CordovaActivity;
public class BrightCity extends CordovaActivity
{
public BrightCity()
{
}
public void onCreate(Bundle bundle)
{
super.onCreate(bundle);
bundle = getIntent().getExtras();
if(bundle != null && bundle.getBoolean("cdvStartInBackground", false))
moveTaskToBack(true);
loadUrl(launchUrl);
}
}
由于该应用程序使用Android Cordova,因此几乎所有代码都不在Java中。相反,当我转移到 assets / www 目录时,我发现了所有使应用程序运行的HTML / JS。

我打开了 editlockbox.js 文件,并查看了一下包含一些API请求的JavaScript:
$.getJSON(url + '/api/LockboxInsurance/GetLockboxInsuranceCompanies', function(companiesJsonPayload) {
$('#companyid').append("<option value='0'>Select Insurance Company</option>");
$(companiesJsonPayload).each(function(i, item) {
$('#companyid').append('<option value="' + item.id + '">' + item.name + '</option>');
});
$('#companyid').append("<option value='Add'>Add New Company</option>");
});
var userid = window.localStorage.getItem("userid");
$("#itemid").val(id);
$("#userid").val(window.localStorage.getItem("userid"));
$.getJSON(url + '/api/lockbox/getlockbox/' + id + '', function(result) {
if (result == null) {
$('#result').append('<li>Item does not exist</li>');
} else {
$("#userid").val(window.localStorage.getItem("userid"));
$("#name").val(result.name);
$.getJSON(url + '/api/lockboxcategory/getlockboxcategories', function(categoriesJsonPayload) {
$(categoriesJsonPayload).each(function(i, item) {
if (item.id == result.categoryid) {
$('#categoryid').append('<option value="' + item.id + '" selected>' + item.name + '</option>');
} else {
$('#categoryid').append('<option value="' + item.id + '">' + item.name + '</option>');
}
});
});
$.getJSON(url + '/api/LockboxInsurance/GetUserPolicies/' + userid + '', function(companiesJsonPayload) {
$('#insurance').append("<option value='0'>Select an insurance policy (choose one)</option>");
$(companiesJsonPayload).each(function(i, item) {
if (item.PolicyID == result.policyid) {
$('#insurance').append('<option value="' + item.PolicyID + '" selected>' + item.CompanyName + "-" + item.PolicyNumber + '</option>');
} else {
$('#insurance').append('<option value="' + item.PolicyID + '">' + item.CompanyName + "-" + item.PolicyNumber + '</option>');
}
//$('#insurance').append('<option value="' + item.PolicyID + '">' + item.CompanyName + "-" + item.PolicyNumber + '</option>');
});
$('#insurance').append("<option value='Add'>Add new policy</option>");
});
$("#description").val(result.description);
$("#serial").val(result.serial);
$("#make").val(result.make);
$("#model").val(result.model);
$("#caliber").val(result.caliber);
$("#additionalinfo").val(result.additionalinfo);
$("#room").val(result.roomlocation);
$.getJSON(url + '/api/lockbox/GetUserLocationRooms/' + userid + '', function(companiesJsonPayload) {
$('#room').append("<option value='0'>Select location of item (choose one)</option>");
$(companiesJsonPayload).each(function(i, item) {
if (item.Name == result.roomlocation) {
$('#room').append('<option selected value="' + item.Name + '">' + item.Name + '</option>');
} else {
$('#room').append('<option value="' + item.Name + '">' + item.Name + '</option>');
}
});
$('#room').append("<option value='Add'>Add new location</option>");
});
//$("#insurance").val(result.policyid);
$.getJSON(url + '/api/photo/getphoto/' + id, function(photoresult) {
if (photoresult.name != "") {
var image = document.getElementById('thumb');
var imageData = iurl + '/upload/lockbox/' + id + '/thumb/' + photoresult.name;
image.src = imageData;
image.style.display = 'block';
//$('#showthumb').html('<img src="' + iurl + '/upload/lockbox/' + id + '/thumb/' + photoresult.name + '" style="width:56px; height:56px; border-radius: 28px; -webkit-border-radius: 28px; -moz-border-radius: 28px;" />');
}
});
$.getJSON(url + '/api/photo/getdoc/' + id, function(docresult) {
if (docresult != null) {
alert(docresult.name);
var doc = document.getElementById('doc');
var docData = iurl + '/upload/lockbox/' + id + '/doc/' + docresult.name;
doc.src = docData;
doc.style.display = 'block';
//$('#showdoc').html('<a href="' + iurl + '/upload/lockbox/' + id + '/doc/' + docresult.name + '" style="width:56px; height:56px; border-radius: 28px; -webkit-border-radius: 28px; -moz-border-radius: 28px;">' + docresult.name + '</a>');
}
});
//$.getJSON(url + '/api/lockboxinsurance/GetInsuranceDetails/' + id, function (insresult) {
// if (insresult != null) {
// $("#policynumber").val(insresult.PolicyNumber);
// $("#companyid").val(insresult.CompanyID);
// }
//});
}
});
您可以看到,所有请求都以类似的方式进行,没有任何身份验证或会话状态机制。接下来,我写了一个快速脚本来搜索所有的JavaScript文件,以生成所有端点的列表:
/api/agency/ /api/agency/getagency/ /api/brightcityapp/ /api/brightcityapp/geteventdetails/ /api/brightcityapp/geteventsforagency/ /api/brightcityapp/geteventsforagencybydaterange/ /api/brightcityapp/geteventsforagencybyloadcount/ /api/brightcityapp/geteventsforagencynew/ /api/brightcityapp/geteventsignupdetails/ /api/brightcitypayments/getcitypaymentsbydaterangenew/ /api/brightcitypayments/getcitypaymentsbyloadcountnew/ /api/brightcitypayments/getcitypaymentsnew/ /api/brightcitypayments/getcityutilpaymentsnew/ /api/brightcitypayments/geteventpaymentdetails/ /api/brightcitypayments/geteventpaymentsbydaterangenew/ /api/brightcitypayments/geteventpaymentsbyloadcountnew/ /api/brightcitypayments/geteventpaymentsnew/ /api/brightcitypayments/geteventutilpaymentsnew/ /api/brightcitypayments/getpaymentdetails/ /api/business/getbusinessesbyagency/ /api/businesswatch/ /api/businesswatch/cancelbusinesswatch/ /api/businesswatch/getbusinesswatch/ /api/businesswatch/getbusinesswatchesforagencybydaterangenew/ /api/businesswatch/getbusinesswatchesforagencybyloadcountnew/ /api/businesswatch/getbusinesswatchesforagencynew/ /api/businesswatchstatus/ /api/businesswatchstatus/getbusinesswatchupdates/ /api/city/getcitiesbyagency/ /api/country/ /api/eyecolor/ /api/gender/ /api/glass/ /api/haircolor/ /api/height/ /api/homewatch/ /api/homewatch/cancel/ /api/homewatch/cancelhomewatch/ /api/homewatch/gethomewatch/ /api/homewatch/gethomewatchesforagencybydaterangenew/ /api/homewatch/gethomewatchesforagencybyloadcountnew/ /api/homewatch/gethomewatchesforagencynew/ /api/homewatchstatus/ /api/homewatchstatus/gethomewatchupdates/ /api/house/gethousesbyagency/ /api/lockbox/ /api/lockbox/deletelockbox/ /api/lockbox/getlockbox/ /api/lockbox/getlockboxesforagencybydaterangenew/ /api/lockbox/getlockboxesforagencybyloadcountnew/ /api/lockbox/getlockboxesforagencynew/ /api/lockboxcategory/ /api/lockboxcategory/getlockboxcategory/ /api/lookout/ /api/lookout/getlookout/ /api/lookout/getlookoutsforagencybydaterangenew/ /api/lookout/getlookoutsforagencybyloadcountnew/ /api/lookout/getlookoutsforagencynew/ /api/lookoutinfo/ /api/lookoutinfo/getlookoutinfoforlookout/ /api/maintenance/ /api/maintenance/cancel/ /api/maintenance/getmaintenance/ /api/maintenance/getmaintenanceforagencybydaterangenew/ /api/maintenance/getmaintenanceforagencybyloadcountnew/ /api/maintenance/getmaintenanceforagencynew/ /api/maintenance/getpublicmaintenanceforagencybydaterangenew/ /api/maintenance/getpublicmaintenanceforagencybyloadcountnew/ /api/maintenance/getpublicmaintenanceforagencynew/ /api/maintenancestatus/ /api/maintenancestatus/getmaintenancestatusforrequest/ /api/message/ /api/message/getmessage/ /api/message/getmessagesforofficerbydaterangenew/ /api/message/getmessagesforofficerbyloadcountnew/ /api/message/getmessagesforofficernew/ /api/newsfeed/getagencynewsfeedslist/ /api/officer/ /api/officer/getofficer/ /api/openrecord/acceptopenrecordrequest/ /api/openrecord/getopenrecord/ /api/openrecord/getopenrecordsforagency/ /api/openrecord/getopenrecordsforagencybydaterange/ /api/openrecord/getopenrecordsforagencybyloadcount/ /api/patrolrequest/acceptpatrolrequest/ /api/patrolrequest/getpatrolrequest/ /api/patrolrequest/getpatrolrequestsforagencybydaterangenew/ /api/patrolrequest/getpatrolrequestsforagencybyloadcountnew/ /api/patrolrequest/getpatrolrequestsforagencynew/ /api/photo/getdoc/ /api/photo/getmaintenancephoto/ /api/photo/getphoto/ /api/race/ /api/scamalert/ /api/scamalert/deletescamalert/ /api/scamalert/getscamalert/ /api/scamalert/getscamalertsforagencybydaterangenew/ /api/scamalert/getscamalertsforagencybyloadcountnew/ /api/scamalert/getscamalertsforagencynew/ /api/skintone/ /api/state/ /api/state/getstate/ /api/support/ /api/trafficalert/ /api/trafficalert/deletetrafficalert/ /api/trafficalert/gettrafficalert/ /api/trafficalert/gettrafficalertsforagencybydaterangenew/ /api/trafficalert/gettrafficalertsforagencybyloadcountnew/ /api/trafficalert/gettrafficalertsforagencynew/ /api/user/ /api/user/getuser/ /api/weatheralert/ /api/weatheralert/deleteweatheralert/ /api/weatheralert/getweatheralert/ /api/weatheralert/getweatheralertsforagencybydaterangenew/ /api/weatheralert/getweatheralertsforagencybyloadcountnew/ /api/weatheralert/getweatheralertsforagencynew/ /api/weight/
此外,我在我的帐户下设置了一个“锁箱”,并上传了一些图像来测试其功能。而对于我的目录列表被允许查看这一点我没有丝毫的意外,这意味着应用程序中所有上传的文档和图像都可以被公开访问。
风险
毫无疑问,使用此应用程序的公众面临的风险非常多并且十分严重。应用程序本身存储的敏感信息,攻击者可以轻易获得,进而可以以其他用户(或警察机构)的身份向系统内提交欺诈性的活动和事件。
没有基本的认证要求,任何应用程序信息或操作/事件的完整性都不能保证是合法的。必须要清楚的一点是,这个应用程序中包含有用户密码(和其他个人信息),可疑人员的常驻报告,公民电子目录,甚至本系统中存储和使用的付款信息,而这些都是不安全的。
同样需要注意的是,其对于用户的影响可能远远超过Bright City系统。用户在多个系统中使用相同的密码是常见的,因此以明文形式显示用户密码可能导致其他帐户(例如电子邮件,银行,社交媒体)的一一告破。
报告
目前我已经就如何以最有效的方式解决这些问题进行了多次的推敲理论。在我的其他博客文章中,我总是尽力与供应商合作,报告和修补已识别的漏洞 – 但这次有点不同。我积极的向市政府报告了上述问题,我们有一个简短的通话来对此进行讨论。尽管供应商最终将整个应用程序脱机以实现身份验证,但我报告的其他许多问题仍未解决(包括目录列表问题)。
编者:在各类智慧城市、智慧社区等概念大举进入我们的生活中时,我们仍然需要时时刻刻的去关注安全问题,智慧化智能化的设备、程序固然能够使我们的生活更为便捷,但安全出了问题也可能让我们的生活陷入一片黑暗。