I recently conducted a penetration test of a web application. Because of design decisions, I was able to bypass CAPTCHA to brute force user accounts and, ultimately, bypass file upload restrictions to upload malware onto the web server and into the internal network environment.
The owner had taken a healthy view of security, had conducted frequent vulnerability assessments, and wanted a full pentest. As expected, a quick web app vulnerability scan showed minimal findings, a possible DOM-based XSS that turned out to be a false positive and some scattered SSL/TLS certificate findings that presented minimal risk to this business.
I set aside the vuln scanner and actually looked at the code. Upon doing so, I found several critical vulnerabilities that needed to be addressed immediately. All of the issues had a common theme, the web application relied solely on client-side security.
What is Client-Side Security?
Client-side processing, in general, is an application design principle that moves app functionality into the client, or user’s computer. In a client-server relationship of this type, the client conducts some level of application processing on the user’s computer before sending information to the server for further processing. Client-side processing has many benefits, including relieving processing stress on the server and creating a customized user experience.
When security is placed client-side, a problem arises. The security controls are now in the hands of the user. Most users are not malicious nor technically capable of exploiting this control, but a skilled malicious user can easily bypass controls that are placed in their hands.
Specific Examples
Multiple vulnerabilities existed in this web app, but two serve as good examples of bypassed Client-side security: CAPTCHA with a client-side invocation and file upload functionality with client-side file restrictions.
CAPTCHA with Client-Side Invocation
The web application allowed anyone to create a user profile. Once created, the user profile was protected with a username and password. After two unsuccessful login attempts, a CAPTCHA was generated. The CAPTCHA was designed so that a human could read the image text and submit the text along with the user credentials. Conversely. the CAPTCHA image was designed to be difficult for a computer to read. As such, a CAPTCHA is a very good security measure to prevent automated brute force login attempts. If a computer cannot read the CAPTCHA image, it cannot submit the text, and cannot attempt a login.
The web application CAPTCHA, itself, worked just fine. The CAPTCHA was generated and processed on the server-side. The invocation of the CAPTCHA, however, was client-side. The code to keep track of unsuccessful login attempts was maintained and sent via client-side session cookies.
As a proof of concept attack, a test user account was created. The POST request for the login page, without CAPTCHA, was copied. The POST request was sent to the web application server 1000 times with different and incorrect password attempts. At the end of the 1000 password attempts, the correct password was attempted and login was successful. The entire proof of concept was conducted in less than a minute.
While the CAPTCHA itself worked as designed, the invocation was kept client-side. I bypassed the invocation by sending the same initial POST request over and over. The session cookie was never updated with the unsuccessful login count, the CAPTCHA was never invoked, and the security mechanism was rendered useless. As the web application profiles contained sensitive financial user information, a malicious actor could have exploited this vulnerability to brute force multiple accounts using frequently used passwords or brute force a single account until the correct password was guessed.
File Upload with Client-Side File Restrictions
The user profile functionality allowed a profile picture to be uploaded. The web app restricted the file uploads to only JPEG, GIF, and PNG file types. Here again, the file restrictions were maintained with client-side security through a simple JavaScript whitelisting of .jpg, .gif, and .png file extensions.
While file restrictions based solely on extensions is, itself, not an ideal security check, the fact that the sole security check was placed into the hands of the user via JavaScript was concerning. Using nothing more than the web browser, I was able to right-click on the web page, select the “Inspect Elements,” and change the whitelist from “.jpg, .gif, .png” to “.exe, .bat, .com”. With that change, I could upload any manner of malware or remote shellcode. As the scope of this assessment precluded actual malware or hacking tool uploads, I loaded the eicar.com virus test file. This triggered a simple antivirus on the server, thus showing File Restriction Bypass.
While it was out of scope for this assessment, the company was well aware that this sort of vulnerability could allow for a malicious actor to upload various malware and hacking tools, including ransomware and remote shell tools.
Conclusion and Recommendations
None of this is to say that client-side security is worthless. Proper implementation of client-side security paired with server-side security can prevent unintentional and unskilled attacks. A reliance on client-side security, without server-side security, is easily bypassed by a modicum of skill, however.
In the case of these specific vulnerability examples, the number of unauthenticated login attempts and invocation of the CAPTCHA should have been kept track of and invoked by the server. And while the File Upload functionality could keep the client-side security checks, a second set of checks should have been performed by the server.
Client-side security relies on trust in the user: trust that the user has no ill intention, will make no mistakes, nor has the skill to attack. If the web application designers trust the user implicitly, no security is required. As business requires proper security, client-side security should not be relied upon and server-side security checks should always be in place.