Jack O'Sullivan
March 22 2021
This one deals with the "new" application fingerprinting check that I discussed. I wrote "new" there because I blogged about it in 2016 and had a PoC tool out there called git-version. As I was going to talk about it at BSides I decided to improve the PoC and make something that should hopefully be more usable.
If you don't care about how it was made, then go straight to get the tool:
https://github.com/secarmalabs/git-fingerprint
The README.md has install instructions and how to launch the command prompt. The suggested workflow in the welcome message shows how to use the script.
To test the script I cloned a copy of "CVE-Offline" (a greppable form of CVE details). I then copied the "cve-offline" folder to "cve-offline-old". I used git "checkout" to rollback the old folder to a previous commit and then I started an HTTP listener to share that folder. From there I launched "git-fingerprint" and configured it using these commands:
1
2
3
4 |
set_repo_path .. /cve-offline
set_target_url http: //localhost/
set_files_and_commit_count
fingerprint_version
|
The following screenshot demonstrates the output from the fingerprinting action:
The highlighted part shows that the target website was using an outdated version of cve-offline.
That should be enough to get you going.
I recommend the rest of this blog only for the brave souls. Intrepid adventurers looking to learn about the technique, design goals, and get a free tutorial in the Python CMD2 module to boot. Should take about 15-20 minutes to read.
If the application you are targeting is powered by code which you can download the Git repository for. Then you can fingerprint version information using the specific commit dates of files. To do this you:
The above algorithm is the basis of the technique. It is also true that the same algorithm will work for other version control systems (VCS). It is just that I have made the PoC work for Git with that being the most popular VCS at the moment.
Caveat; Files that this will work for are those which are not altered by the download process. PHP/JSP/ASPX etc will all alter to return HTML code instead of the committed file contents. Good candidates are ".js", ".txt", etc.
The technique is generic and will work without the need to maintain a database of regular expressions to detect minor differences in files. Pre-existing fingerprinting techniques rely on such databases to succeed.
As a professional pentester (or bug bounty ninja) you need to fingerprint the software versions in use by your target. Armed with an accurate version you can:
Some targets will have been hardened to prevent trivial version number leaks via HTTP headers, HTML comments etc. This is good because it limits information to an attacker or so it is said.
As I was going to talk about it at BSides I decided to improve the PoC and make something that should hopefully be more usable. Key parts of the upgrade included:
Basically, take the parts that were done using bash and do them within the script, and improve the output.
During my development time I came across the Python CMD2 module. This enables simple command prompt interfaces to be created and does a lot of heavy lifting. I always enjoyed things like the Social Engineering Toolkit which adopt that input approach. I think that makes it easier for a fully featured framework to grow up around a kernel of functionality, so I was interested in trying that out.
Benefits of CMD2:
Essentially I fell in love with CMD2 during early development and couldn't back out. The final section of this blog is an ode to CMD2. It also explains the structure of "Git-Fingerprint" at the same time. Why just drop a tool and not discuss how it is put together for the curious?
You can install cmd2 via pip. As I am using Python3 you would need to use the command shown below:
1 |
pip3 install cmd2
|
To access that module within your ".py" simply import it using these lines:
1
2 |
import cmd2
from cmd2 import Cmd, with_argparser, with_argument_list, with_category
|
Then declare your class and pass it cmd2.Cmd:
1
2
3
4
5
6
7
8
9 |
class Interface(cmd2.Cmd):
"""
Interactive command prompt enabling you to fingerprint a web application version using git
"""
prompt = "Git-Fingerprint> "
CMD_CAT_GIT_VERSION = "Git Version Commands"
def __init__( self ):
cmd2.Cmd.__init__( self )
|
The multiline string here is part of the self-documentation. This string will be displayed whenever the command line interface is launched.
The "prompt" text will be displayed when your script is waiting for user input.
While I was working on Git-Fingerprint I asked the CMD2 project to support categorisation of commands. This was a ticket they had already been asked for and god love them they implemented it the week I asked :D. Much love to the CMD2 maintainers.
A category will control the output of the "help" command. Built in commands from CMD2 will be displayed under one category and your commands can be shown in logical chunks. The following demonstrates that:
To enable that ensure that "with_category" is imported and define your categorisation text using a variable such as "CMD_CAT_GIT_VERSION" as shown previously.
To define a new command inside your interface use code like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13 |
# sets the local path to a repository if you
# prefer that workflow
@with_argparser (set_repo_path.get_argparse())
@with_category (CMD_CAT_GIT_VERSION)
def do_set_repo_path( self , args):
"""
Sets the local path to a repository
"""
if os.path.exists(args.path) and os.path.isdir(args.path):
globalvars.repo_path = args.path
print ( "[*] Local Repo Path set to: " + globalvars.repo_path)
else :
utils.print_error( "Supplied path does not exist or is not a directory" )
|
The "@with_argparser" directive takes an ArgParser object and is used to define the inputs and help pages for a command. This is a standard library for defining how to interact with Python scripts. In an attempt to build a framework I located the argparse definitions for this command in a seperate ".py" file. Smaller files with dedicated tasks is *probably* more maintainable.
Any function prefixed with "do_" are picked up by CMD and become commands. The command name will be the text after that prefix so our command is "set_repo_path".
The "@with_category" directive takes the variable "CMD_CAT_GIT_VERSION" that we declared earlier. This marks the "do_set_repo_path" action as part of Git-Fingerprints commands when the "help" command is executed.
The multiline comment at the top becomes the short description of the "do_set_repo_path" command when the "help" command is executed. While the full output of the argparse definition will be displayed if "help set_repo_path" is executed as shown below:
At the end of the interface class definition I also added these important lines:
1
2 |
# enable path completion for commands that need it
complete_set_repo_path = cmd2.Cmd.path_complete
|
The previous code block showed the definition of the "set_repo_path" command. Custom commands do not have OS path completion when the user hits "tab" unless you turn it on. The above command is all that you need to turn on tab completion.
As the "set_repo_path" command needs to specify an OS path tab completion was essential.
The above has shown how to:
The final glue is how to start the interface. At the bottom of the script outside of the "Interface" indenting add this code:
1
2
3 |
if __name__ = = '__main__' :
app = Interface()
app.cmdloop()
|
This creates a new instance of the "Interface" class and then starts the CMD2 command loop.
For completeness here is the full listing (you can get the "set_repo_path" file from the Git-Fingerprint repositorty and place it in the same directory if you want to see this work):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 |
# cmd2 imports
import cmd2
from cmd2 import Cmd, with_argparser, with_argument_list, with_category
# my class
import set_repo_path
class Interface(cmd2.Cmd):
"""
Interactive command prompt enabling you to fingerprint a web application version using git
"""
prompt = "Git-Fingerprint> "
CMD_CAT_GIT_VERSION = "Git Version Commands"
def __init__( self ):
cmd2.Cmd.__init__( self )
# sets the local path to a repository if you
# prefer that workflow
@with_argparser (set_repo_path.get_argparse())
@with_category (CMD_CAT_GIT_VERSION)
def do_set_repo_path( self , args):
"""
Sets the local path to a repository
"""
if os.path.exists(args.path) and os.path.isdir(args.path):
globalvars.repo_path = args.path
print ( "[*] Local Repo Path set to: " + globalvars.repo_path)
else :
utils.print_error( "Supplied path does not exist or is not a directory" )
# enable path completion for commands that need it
complete_set_repo_path = cmd2.Cmd.path_complete
if __name__ = = '__main__' :
app = Interface()
app.cmdloop()
|
Hopefully this has explained the inner workings and means you can go ahead and fix my coding and submit the required updates for me :D
Happy Git Fingerprinting!