Introduction:
I took "Application Security: for Hackers and Developers" class at Derbycon this year. More about the course is here:
https://www.vdalabs.com/expert-training/security-training-courses/security-for-hackers-and-developers/ Videos are also available on PluralSight. Anyways, the class is focused on searching for vulnerabilities by using various methods such as static analysis, dynamic analysis, binary analysis, fuzzing, and etc.
For a class I was taking this semester, I decided to write my final paper on different techniques used to review code and/on find security issues. I also decided to look at a project that I worked on a long time ago for the hands-on part.
The project is runthelabs (can be found here:
https://github.com/BoredHackerBlog/runthelabs). It is a Python+Flask based web app that takes in a JSON configuration file and creates a virtual environment. It uses Minimega (
minimega.org), KVM, OpenVSwitch, and NoVNC. It also uses SQLite for holding data.
The point of it is that a teacher creates a JSON file with virtual environment specifications, uploads it, and starts the lab. The teacher can copy and send NoVNC links to students/groups so they can VNC into a VM and work on whatever. More info here:
https://github.com/BoredHackerBlog/runthelabs/tree/master/documentation
When I put the project on Github, I knew it could have some kind of injection vulnerability. The code was written so long ago and I never got to updating everything. (laziness is not good for security)
Goal/Testing Purpose and Scope:
The goal of this testing is to apply code review and security testing techniques and find security issues in my project. The scope is just my application/code. Third-party code or issues related to Minimega, KVM, OpenVSwitch, and etc. are not a concern.
Software Internals:
api.py is the main flask app. There is a webUI and an API way of interacting with the app.
config.py stores config information (paths to files and etc...)
dbcontro.py is responsible for interacting with SQLite DB.
mmcontrol.py is responsible for executing commands in relation to minimega, iptables, and openvswitch.
mmstart.py parses JSON file uploaded by the admin and uses mmcontrol to set things up.
There are two user roles. One admin and the other one is student/unauthenticated.
Here's what the admin does: Uploads a JSON config file and starts the lab (which turns on VM's and sets up networking). Optionally, the admin can turn the whole lab off, reboot VM, change VNC password, and finally, share NoVNC link with the student.
VNC can be accessed via realvnc or other VNC software with the correct port and password or NoVNC.
Unauthenticated user: They can check server status (if it's up or not. Not very useful) and access VM's via VNC, if they have URL or password+port.
The software uses SQLite DB to keep track of VM name, password, and port.
Port 1337 is used for WebUI and API. Port 1338 is used for Websockify/NoVNC.
Testing Setup:
To set up a testing environment, I needed one server to the run web app and two machines. One for static analysis/dynamic analysis/hacking and the other one for Admin/Teacher role.
Static Analysis:
Static analysis is analyzing the code without running it. Here are useful OWASP links:
https://www.owasp.org/index.php/Static_Code_Analysis &
https://www.owasp.org/index.php/Source_Code_Analysis_Tools
I started by using bandit (
https://github.com/PyCQA/bandit) to scan my code. Here are some of the issues:
- Subprocess module is in use
- Hardcoded password
- Use of md5 function (used to generate VNC password, not a vuln)
- Binding to all interfaces
- Starting a process with shell (using os.system)
- Starting a process with shell, with possible injection (it detects when external variables are used)
These tools are definitely useful for a larger project. They did find useful things.
Here's what OWASP recommends focusing on:
OWASP also recommends looking at inputs and data flow. They have more things recommended but I wanna try to focus on vulnerability areas the above screenshot mentions and inputs.
- Data Validation: There isn't any. json.loads is used, which validates that the upload is json, however, that doesn't really matter. For starting a lab, JSON data has to be correct, however, values don't have to be. If something is int, string, or etc. it isn't checked. The uploaded file isn't saved on disk either.
- Authentication: Admin has to login to use WebUI or API. api.py has a hardcoded password.
- Session Management: Basic auth is used, so there isn't any.
- Authorization: N/A
- Cryptography: The WebUI/API access does not use SSL/TLS neither does the NoVNC connection. If someone was eavesdropping, they could get credentials.
- Error Handling: Yes! I'm doing try-except then returning a generic error message. Also, in the try section, I'm doing If and returning a generic message. It's not perfect. There are some flaws.
- Logging: None
- Security Configuration: N/A
- Network Architecture: The web app does bind to all interfaces.
As for inputs, only admin has input capabilities. They can upload JSON file, reboot VM's, and change VM VNC password.
Here's the example config file:
For JSON file upload, it's done through /upload. The file is assigned to labconfig variable. When the lab is turned on (through /on), startlab() is called, which creates a db and calls mmstart.startmm(labconfig). startmm calls mmcontrol.start_mm(), which starts Minimega. After that, JSON file is processed. First thing looked at is gre, then dhcp, then internet, then finally VM. For VM, mmcontrol.vm_config ends up being called, which runs os.system statement with networking info. With JSON processing, there are several places a command could be injected.
For VM reboot, here's what ends up being ran, when vmname is supplied via GET request:
mmcontrol.vm_reboot(vmname, dbcontrol.get_password(vmname)), and in vm_reboot() this statement is executed first: os.system(minimega_cmd + "vm kill " + vm_name). Injection can happen here.
For VM VNC password reset, mmcontrol.set_password() is used, which executes os.system(minimega_cmd + "vm qmp " + vm_name + " " + json.dumps(vncpwcmd)) first. In this case, the injection could occur in the middle of the statement.
Dynamic Analysis:
For dynamic analysis, the application has to be running. I used OWASP ZAP, Subgraph Vega, and Nikto. They didn't actually find anything useful, which is expected, however, Nikto did guess hardcoded login admin/admin.
I started doing manual analysis. I would use Burp but it really isn't needed for now.
First, I uploaded a random file, which didn't do anything. I uploaded a random JSON file, and it was accepted. The labs wont start obviously since it's not a valid config file. After that, I took the example config file and injected commands. Here's what the new file looks like:
The command injection worked:
After this, I uploaded the example JSON configuration which was included and started the lab the way it should be so I can try to mess with GET parameters.
First the reboot:
It worked:
Next, the VNC password reset:
That also worked:
Another issue with this software is cross-site request forgery. Since I'm not using session management or any other security, when a request is made via another webpage, if the admin is logged in, the request will get processed.
For example: <img src="http://10.0.0.53:1337/reboot/tc2" width="0" height="0" border="0"> embedded in another HTML page does cause a reboot for tc2. Of course, since I wasn't doing any checking to see if VM name is a valid name, the attacker, does not need to know vm name to execute commands.
Here's my new code, asdf vm doesn't exist:
I logged into the admin account on another machine and opened the poc webpage:
On my "hacker" machine:
Dynamic analysis helps with confirming/validating some of the findings in static analysis.
Findings:
- Binding to all network interfaces
- This is bad because depending on the network configuration, the webui can be accessed from inside the VM
- Command injection
- Bad but only admin can do it (unless CSRF is used)
- Lack of data validation
- I should have validated everything in JSON file and even data from GET requests. For example, if vmname is supplied for reboot, I should check to see if that VM exists or not. I should make sure that I only allow a-z,A-Z,0-9 as input chars.
- Bad session management
- I probably shouldn't have used basic auth. Flask (or modules on top of Flask) has session management mechanisms that I could have used. CSRF token should be used. There are other web app protections that could be used as well.
- Cross-site request forgery
- CSRF token should be used.
- Lack of cryptography
- The way I imagined this web app would be used didn't require adding ssl/tls protection but it's still something I wanted to point out.
- Error handling could be better
- Error messages are generic. More detailed messages would be useful. Also, more error checking should be done. For example, if someone starts a lab with bad json file, code still starts minimega binary. That should not happen. Return from os.system should be checked too.
- Lack of logging
- I should have been logging some stuff, mainly errors.
Basically, there are three ways to get root on the system running runthelabs. A non-admin user can use CSRF w/ command injection. A malicious admin can use various command injection points. Finally, a MITM attack can be used to capture admin credentials and those could be used to execute a command injection attack.
Conclusion:
It's possible that I may have missed something. Static and dynamic analysis both definitely were useful. OWASP is a great resource on code review. Also, this
Github awesome list is very useful:
https://github.com/mre/awesome-static-analysis
The security issues occurred due to laziness and the risk/chances of exploitation were low. Also, I accepted the risk of possible command injection by the admin when I was programming. The impact is high since you can get root pretty easily with CSRF or if you were a malicious admin.