I forgot my password to my Unifi controller


Backstory

Recently I purchased some Unifi gear, and I installed the controller software mainly as a test, and thus I did not immediately save the username and password in a password manager but instead I probably wrote it down in Windows Notepad, and Windows 10 being a total dick, just one day restarted losing what I had in notepad.

Some time passed, and I forgot not only the password, but also the username. Then I realized that once the Unifi gear is adopted to the controller, it would require some effort to re-adopt and configure the gear into a new instance of the controller - so I was left scratching my head as to how to break in.

Lesson learned: recognize the fact that a “test installation” might be promoted to production.

Here’s a small explorative story of my thought process and how I managed to “break in” back to my Unifi controller. Quotes, because it’s not much hacking when I had root access to to the VM the controller was running in.

It’s also a side story of the powerful help in MongoDB that helped me go from zero to doing something meaningful in a really short time.

The objective: find out the username and reset the password.

First try: “Forgot password?” dialog

My first instinct was to try the “Forgot password?” feature. I gave the correct email, but the password reset link never came. I guess I should have configured some SMTP defails when I set up the controller..

Second try: explore the filesystem

I didn’t find the user account details by exploring the Unifi controller’s saved files in the filesystem.

Third try: explore the database

I am running the controller as a Docker image from the ever awesome Jessfraz.

After entering the container, $ mongo (the CLI for MongoDB) didn’t exist, so we need to install it:

$ apt search mongo
mongodb-org-server/now 3.4.17 amd64 [installed,local]
  MongoDB database server

I should be able to see more MongoDB related stuff, so because they’re missing we must refresh package index. Also, from the above package we know we probably should search with “mongodb-org”:

$ apt update
$ apt search mongodb-org
mongodb-org/xenial 3.4.17 amd64
  MongoDB open source document-oriented database system (metapackage)

mongodb-org-mongos/xenial 3.4.17 amd64
  MongoDB sharded cluster query router

mongodb-org-server/xenial,now 3.4.17 amd64 [installed]
  MongoDB database server

mongodb-org-shell/xenial 3.4.17 amd64
  MongoDB shell client

mongodb-org-tools/xenial 3.4.17 amd64
  MongoDB tools


# great, so it's mongodb-org-shell

$ apt install mongodb-org-shell

Running $ mongo now works but it didn’t manage to connect to the default TCP port. What port is it running in?

$ netstat -na | grep mongo
unix  2      [ ACC ]     STREAM     LISTENING     22071    /usr/lib/unifi/run/mongodb-27117.sock

Oh it listens only to a Unix socket, so let’s try it:

$ mongo --host /usr/lib/unifi/run/mongodb-27117.sock
MongoDB shell version v3.4.17
connecting to: mongodb://%2Fusr%2Flib%2Funifi%2Frun%2Fmongodb-27117.sock/
MongoDB server version: 3.4.17
Welcome to the MongoDB shell.
For interactive help, type "help".

Awesome, we’re in! Knowing nothing about MongoDB, I appreciate the hint of starting with help:

> help
    db.help()                    help on db methods
    db.mycoll.help()             help on collection methods
    sh.help()                    sharding helpers
    rs.help()                    replica set helpers
    help admin                   administrative help
    help connect                 connecting to a db help
    help keys                    key shortcuts
    help misc                    misc things to know
    help mr                      mapreduce

    show dbs                     show database names
    show collections             show collections in current database
    show users                   show users in current database
    show profile                 show most recent system.profile entries with time >= 1ms
    show logs                    show the accessible logger names
    show log [name]              prints out the last segment of log in memory, 'global' is default
    use <db_name>                set current database
    db.foo.find()                list objects in collection foo
    db.foo.find( { a : 1 } )     list objects in foo where a == 1
    it                           result of the last line evaluated; use to further iterate
    DBQuery.shellBatchSize = x   set default number of items to display on shell
    exit                         quit the mongo shell

Ok so we can start by listing databases:

> show dbs
ace       0.003GB
ace_stat  0.006GB
admin     0.000GB
local     0.000GB

The DBs admin and local are probably some MongoDB system stuff, so we’re left with ace and ace_stat. The latter is probably related to statistics storage, so ace is our best bet.

Again, the help command also told us how to “enter” a database:

> use ace
switched to db ace

The help command also taught us how to list “tables” (collections in MongoDB terminology):

> show collections
account
admin
alarm
broadcastgroup
dashboard
device
...
scheduletask
setting
site
stat
tag
task
user
usergroup
verification
virtualdevice
voucher
wall
wlanconf
wlangroup

We’re probably interested either in admin, account or the user collection.

Conveniently, the same top-level help also taught us how to list objects in the collection:

> db.user.find()
{ "_id" : ObjectId("5b94d704c8df8b0001ab6b5a"), "mac" : "08:00:27:...", "site_id" : "5b94d698c8df...", "oui" : "PcsSyste", "is_guest" : false, "first_seen" : NumberLong(1536481028), "last_seen" : NumberLong(1538481428), "is_wired" : true, "name" : "...", "usergroup_id" : "", "noted" : true, "hostname" : "..." }
much more rows like above... (some data was masked for my privacy)

Ok those are devices in my network, so that’s the wrong collection. Next we’ll try admin:

> db.admin.find()
{ "_id" : ObjectId("5b94d6d1c8df8b0001ab6b4e"), "email" : "...", "last_site_name" : "default", "name" : "joonas", "time_created" : NumberLong(1536480977), "x_shadow" : "$6$dCFzb1Vb$D2AV2BnF2.hbLY3tMD7w4c5Xbjz/..." }
(again, some data masked with dots for privacy)

Bingo! And don’t bother with even the cut portion of my password hash, because I never use the same password twice.

The password is stored in field x_shadow. The hash looks familiar but I don’t know it from the top of my head. A quick Bing search (jk) for $6$ hash landed me here, so it’s sha-512. After installing $ mkpasswd (see the previous link for how):

$ mkpasswd -m sha-512 test
$6$5xoqSMQ32wdPR$RDcGgk2Jd6LvkkzCSaJJ0s3o73IW.ikMIzSusqOwL3G908XLG4OGjCfOewePNJEYAbiFyKS9p0hoPBTvxMvr61

Structurally the hashes look the same, so now all we have to do is plug a new hash in to the database to effectively reset the password. How to update an object in a collection?

The help command had this row:

db.mycoll.help()             help on collection methods

So, running this:

> db.admin.help()
...
    db.admin.update( query, object[, upsert_bool, multi_bool] ) - instead of two flags, you can pass an object with fields: upsert, multi
    db.admin.updateOne( filter, update, <optional params> ) - update the first matching document, optional parameters are: upsert, w, wtimeout, j
...

The two above functions look like something we can work with. Let’s just try something:

> db.admin.updateOne({name: 'joonas'}, {x_shadow: '$6$5xoqSMQ32wdPR$RDcGgk2Jd6LvkkzCSaJJ0s3o73IW.ikMIzSusqOwL3G908XLG4OGjCfOewePNJEYAbiFyKS9p0hoPBTvxMvr61'})
E QUERY    [thread1] Error: the update operation document must contain atomic operators

Ok we’re doing something wrong here, so opening the docs for updateOne, we probably need to use $eq as query operator and $set as update operator:

> db.admin.updateOne({$eq:{name: "joonas"}}, {$set:{x_shadow: "$6$5xoqSMQ32wdPR$RDcGgk2Jd6LvkkzCSaJJ0s3o73IW.ikMIzSusqOwL3G908XLG4OGjCfOewePNJEYAbiFyKS9p0hoPBTvxMvr61"}})
WriteError({
        "index" : 0,
        "code" : 2,
        "errmsg" : "unknown top level operator: $eq",
        "op" : {
                "q" : {
                        "$eq" : {
                                "name" : "joonas"
                        }
                },
                "u" : {
                        "$set" : {
                                "x_shadow" : "$6$5xoqSMQ32wdPR$RDcGgk2Jd6LvkkzCSaJJ0s3o73IW.ikMIzSusqOwL3G908XLG4OGjCfOewePNJEYAbiFyKS9p0hoPBTvxMvr61"
                        }
                },
                "multi" : false,
                "upsert" : false
        }
})

The error message says unknown top level operator: $eq, that is strange because $eq is the correct operator so it’s not immediately clear what I did wrong because the error I made should have resulted in error message telling me that $eq is an unknown field, or that name is an unknown operator..

Looking at more docs, the field name should be before the operator in the query part, but the update part was correct ($set operator as outer, and field name as inner):

> db.admin.updateOne({name:{$eq: "joonas"}}, {$set:{x_shadow: "$6$5xoqSMQ32wdPR$RDcGgk2Jd6LvkkzCSaJJ0s3o73IW.ikMIzSusqOwL3G908XLG4OGjCfOewePNJEYAbiFyKS9p0hoPBTvxMvr61"}})
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

Now, trying to log in again, SUCCESS:

Awesome, we just reset the password by poking at the internals. :)

signature