Prelude
So, why did I suddenly post something about an Android app? I’m preparing for something and I won’t disclose it for now. Don’t mind me, and also beware that you’re learning alongside me, I’m nowhere near an expert in this subject matter.
Today we’re going to reverse engineer a Weather app. Credits to HexTree for the resource.
Objectives
- What is the custom API being used by the app?
- What is the authentication mechanism?
- Why are weather updates disabled?
Finding the Custom API
Finding the API isn’t that hard since we can just search for the http/https keyword, because interacting with an API almost always requires a URL. Note that I leveraged Claude to rename the obfuscated function names throughout this writeup.
1
2
3
4
5
6
7
8
9
| public abstract class WeatherFetchThread extends Thread {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
private Location location;
private String zipCode;
public Context context;
// ...
}
|
Focusing on the run() method, we can confirm it’s performing an HTTP request that accepts three parameters, a URL, a User-Agent, and an API key.
1
2
3
4
5
6
7
8
9
10
11
12
| @Override
public void run() {
String url = "https://ht-api-mocks-lcfc4kr5oa-uc.a.run.app/xml/SOAP_server/ndfdXMLclient.php";
if (this.location == null) {
if (this.zipCode != null) {
// build zip-based query
}
onFetchComplete(HttpClient.fetchWithApiKey(url, "HextreeForecastUSA/v4.x", this.context.getString(R.string.ApiKey)));
}
// build location-based query
onFetchComplete(HttpClient.fetchWithApiKey(url, "HextreeForecastUSA/v4.x", this.context.getString(R.string.ApiKey)));
}
|
Reading the source of HttpClient.fetchWithApiKey() confirms this is a standard GET request. The second parameter maps to the User-Agent header, while the third maps to X-API-KEY.
1
2
3
4
5
6
| public static String fetchWithApiKey(String url, String userAgent, String apiKey) throws Throwable {
// ...
httpURLConnection2.setRequestProperty("User-Agent", userAgent);
httpURLConnection2.setRequestProperty("X-API-KEY", apiKey);
// ...
}
|
Searching for ApiKey leads us to strings.xml, the Android resource file where string constants are stored.
1
| <string name="ApiKey">HXT{android-api-key-b1872g}</string>
|
With all three pieces gathered, we can now replicate the request outside the app.
1
2
3
4
5
6
| set BEGIN (python3 -c "from datetime import datetime; import urllib.parse; print(urllib.parse.quote(datetime.now().strftime('%Y-%m-%dT%H:%M')))")
curl -X GET \
"https://ht-api-mocks-lcfc4kr5oa-uc.a.run.app/xml/SOAP_server/ndfdXMLclient.php?whichClient=NDFDgen&zipCodeList=13337&product=time-series&begin=$BEGIN&maxt=maxt&mint=mint&dew=dew&appt=appt&wx=wx&icons=icons&wwa=wwa&Submit=Submit" \
-H "User-Agent: HextreeForecastUSA/v4.x" \
-H "X-API-KEY: HXT{android-api-key-b1872g}"
|
Why Are Weather Updates Disabled?

Tracing the “Weather Updates Disabled” string leads us to refreshForecast().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| private void refreshForecast() {
setForecastButtonEnabled(false);
boolean hasLocation = m.c(this);
String zipCode = m.b(this);
if (!zipCode.equals("13337") && !zipCode.equals("42")) {
Toast.makeText(this, "Weather Updates Disabled", 0).show();
return;
}
if (hasLocation) {
this.forecastManager.i(this.locationManager.b());
} else if (zipCode.length() == 5) {
this.forecastManager.fetchForecastByZip(zipCode);
}
}
|
The logic is straightforward. The app will show “Weather Updates Disabled” unless the zip code is exactly 13337 or 42. However, there’s a catch even if you enter 42 to bypass the first check, it will never actually fetch data because of a secondary validation inside fetchForecastByZip().
1
2
3
4
5
6
| public void fetchForecastByZip(String zipCode) {
if (TextUtils.isEmpty(zipCode) || zipCode.length() != 5) {
return;
}
new ZipFetchThread(zipCode, this.f402a).start();
}
|
42 is only 2 characters so it gets rejected here. The only zip code that satisfies both conditions is 13337, which passes the first check and is exactly 5 digits.
Bypassing the Client-Side Restriction
This length check only exists in the Android app, not on the server. This is a classic broken access control pattern, client-side validation that can be trivially bypassed by going directly to the API. So we can just send 42 via curl and the server will happily accept it.
1
2
3
4
| curl -X GET \
"https://ht-api-mocks-lcfc4kr5oa-uc.a.run.app/xml/SOAP_server/ndfdXMLclient.php?whichClient=NDFDgen&zipCodeList=42&product=time-series&begin=$BEGIN&maxt=maxt&mint=mint&dew=dew&appt=appt&wx=wx&icons=icons&wwa=wwa&Submit=Submit" \
-H "User-Agent: HextreeForecastUSA/v4.x" \
-H "X-API-KEY: HXT{android-api-key-b1872g}" | grep "HXT"
|
1
| <value coverage="chance" intensity="light" weather-type="HXT{android-api-h192gsa0}" qualifier="none">
|
Flag retrieved. The server never validated the zip code length, trusting that the client would enforce it, which it clearly doesn’t when you go around it.