main.dart.js
, which is the Flutter app compiled by dart2js
.AdminPage
, and a /api/flag
endpoint that is fetched using postRequestWithCookies
.LoginPage
, we could see that there is a /api/token
endpoint. This endpoint returns our current authentication token.admin#13371337
user. There were two main features - sending a normal message and sending a poll. apiGet
and apiVote
paths, we get a callback on our server!widget
, apiGet
, and apiVote
paths are appended to the base URL without sanitization - so using @ATTACKER_URL
causes the following URL to be constructed:http://[email protected]_URL
<canvas>
, so rendering unescaped HTML was hopeless.widget
parameter instead.Access-Control-Allow-Origin: *
header. For example, in Flask:/widget/chatmessage
widget and take a look:pollmessage
and imagemessage
widgets.parseLibraryFile
documentation, which seems to provide the most examples.Clipboard_getData
function we found in main.dart.js
.poll.dart
gave us some ideas.ApiMapper
makes a GET request to the specified apiGet
URL. The response data is then saved in data.<dataKey>
, as we can see from the loop accessing data.poll_options
.onPressed
event handler, api_post
, seemingly provides a mechanism for us to exfiltrate our data.example.com
./api/flag
endpoint requires a POST request, and ApiMapper
only does GET requests. Additionally, we needed to make this zero-click.onLoaded
event handler could be used to trigger the api_post
event for zero-click exfiltration. But this was a bit iffy and only worked in some scenarios, such as the following one./api/flag
./ping?id=
endpoint, we get the base64-encoded result of each ping request. Using a sufficiently large id
, we could get an out-of-bound memory read.id
, we would find that the ID range that corresponds to the router's RAM is from 18446744073709551463
to 18446744073709551615
. We could dump out the entire RAM this way.FLAG{r0uter_p0rtals_are_ultimately_impenetrable_because_they_are_real_weird}