Have you ever had an amazing idea for automating two or more pieces of technology and then realized one of them doesn’t have an API?
I came across this problem more than once during the development of a couple of projects here at Praetorian. In this post, I’ll share some of the libraries and techniques I have used to build out APIs for CLI programs, such as HashCat and nmap. Hopefully, these techniques and libraries will be helpful to you when building out new web applications and frameworks.
The first project I worked on at Praetorian was a tool called Project Mars (formerly PWAudit). The goal of the project was to leverage cloud-based GPU systems to crack password hashes while offering users a clean, powerful interface. We decided to use HashCat for the backend hash-cracking tool, and we needed to come up with a way to communicate with multiple cracking sessions for the service to work.
Interfacing with CLI Programs
While researching various ways to interface with CLI programs, I stumbled upon a ruby gem called ruby-nmap. After looking through the source code, I noticed it was using a library, called RProgram, to wrap the nmap binary. RProgram was built to give developers a simple and powerful way to run command line programs from within ruby. It basically allows you to map out any flags as methods to an object. Mapping an entire program is relatively quick, and RProgram includes additional options for multiple arguments per flag, trailing equal signs, and trailing arguments. One thing to note is that arguments are ordered by assignment, so if your program is sensitive to the order of certain flags, make sure you assign the variables in the order you would normally. Here is a small example of how RProgram works:
Define a flag:
short_option :flag => '-o', :name => :outfile
Run it:
prog.run do |p| p.outfile = 'test.txt'end
Result:
./prog -o test.txt
Queueing Asynchronous Tasks
RProgram is a blocking process and runs as long as the CLI program is processing. In order to interact with log files and not block up the API, I use a gem called SuckerPunch to handle asynchronous tasking. SuckerPunch is a pretty simple gem that makes queueing asynchronous tasks very easy. Here is an example of using SuckerPunch:
class Tool def run(async=false) if async Async.new.async.run(self) else sleep 15 $stdout.write "This is Async" sleep 15 end endendclass Async include ::SuckerPunch::Job workers 3 def run(obj) obj.run endendtool = Tool.newtool.run(true)puts "hello"puts "test"sleep 40
This outputs:
hellotestThis is Async
SuckerPunch allows me to parse output and log files while the CLI program is running in the background. I always create a .pid
file in the method that is called async in order to know when the program is running and when the program has finished. One thing to note when using SuckerPunch is that the parent process must remain open/running in order to finish all the async tasks. This works great inside Sinatra web applications, since they are always running. I also use Sinatra to write my REST APIs. It is an amazing framework for deploying web applications quickly with minimal code. You can literally deploy a Sinatra app in four lines of code.
As for SuckerPunch, I have used it in multiple web applications to queue tasks such as emailing and generating files. I usually write a class file to make the entire process an object. It allows me to validate input for flags and keep the code nice and organized. You can view an example in the repo that I have posted along with this post.
Constructing the REST API
Now that you have your wrapper and your program is non-blocking, we just have to create the REST API. When I build REST APIs for CLI programs, they usually consist of three main routes: /start.json
, /status.json
, and /results.json
. Depending on your needs and the features of the program you are interfacing with, you can add additional routes such as /stop.json
, /resume.json
, /delete.json
, etc. Here is an example of a simple status route:
get '/status.json' do content_type :json if File.exists?('prog.pid') return {'status' => 'running'}.to_json else return {'status' => 'complete'}.to_json endend
Sinatra has a very small footprint, so it requires very little to get spun up.
Challenges with HashCat’s Output
While developing the Ruby HashCat API, I ran into somewhat of a problem while creating the wrapper. Unfortunately, HashCat does not output a complete status file, and some statistics are only shown via STDOUT
. I would have preferred some form of a log file that held all the statistics because it is a pain to parse STDOUT
, but in this case I had no other choice. I submitted a feature request to the developers to address this, though my request was denied. HashCat does have a feature to replace the verbose status output with a simplified, machine-readable line, although it still outputs via STDOUT
. I wrote a parser for both methods, and it works.
Currently, the HashCat API supports:
- Starting a new crack job
- Checking the status of a crack job
- Grabbing the results of a crack job
- Cleaning all files from a crack job
Share via: