Web Server

Web Server#

The technology landscape of the web’s server side is broader than for the client side — you’re not limited by what browsers can and are willing to do, any program can act as a web server. This makes the security landscape much broader and includes many types of attacks which are not necessarily specific to the web, but rather are most commonly found on the web, even though they can also be found elsewhere.

Injections#

SQL Injection#

We’ve already mentioned SQL injection earlier. It is caused by improper separation of data/values and SQL commands. When the original query is a SELECT and the data is returned to us, we can usually dump the entire database quite easily, but even in cases when we only know whether an error happened, or even just the time the query took, we can extract most data from the database as well, it will just take time. Since SQL databases usually offer quite a lot of functionality, it is often possible to get a full remote code execution (RCE) or a shell with just SQLi.

A well known tool for extracting data from databases via SQL injection is sqlmap.

NoSQLi Injection#

You may have heard about so-called NoSQL databases, like MongoDB, which don’t store data in tables, but rather in heterogeneous (in case of MongoDB JSON-like) structures. Since NoSQL databases are just all databases which don’t use SQL, the attacks on them can be quite different, depending on how their query language works. As an example, we’ll look at a simple MongoDB query:

Let’s say we have a database of users and we want to find out whether a user-password combination exists and if so, we want to sign the user in. Using the Node.js API, the code might look something like this:

data = JSON.parse(request.body)
db.users.find({
  username: data.username,
  password: data.password
})

When both the username and password are strings, this will work just fine. Since the whole request body is parsed as JSON, we can do whatever we want. For instance, a request like this:

{
  "username": "admin",
  "password": { "$ne": "" }
}

will get passed straight through to find, and the query will be:

db.users.find({
  username: "admin",
  password: { $ne: "" }
})

$ne means “not equals”, so MongoDB will always return an answer (and the application will thus sign the user in), unless admin’s password is an empty string, which is probably a bigger problem.

Server-Side Template Injection#

Server-Side Template Injection (SSTI) is a vulnerability that occurs when user input is embedded into a template not as the input variables, but as a part of the template. Template engines like Jinja2 (Python - used in Flask), Twig (PHP), or FreeMarker (Java) are used to generate dynamic content by combining templates with data.

For example, in a normal use case, a template engine would be used like this:

from jinja2 import Template

template = Template("Hello, {{ name }}!")
output = template.render(name=user_input)

Here, user_input is treated as data and properly escaped. However, SSTI occurs when user input becomes part of the template itself:

template = Template("Hello, " + user_input + "!")
output = template.render()

When attackers can inject template syntax, they may be able to execute arbitrary code on the server.

A simple way to test for SSTI is to inject template expressions and see if they get evaluated. For example, in a Jinja2 template, you might try:

{{ 7*7 }}

If the server returns 49 instead of the literal string {{ 7*7 }}, you know the template engine is evaluating the input. From there, attackers can often escalate to full remote code execution by accessing internal objects and methods. In Jinja2, a common payload to achieve RCE might look like:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

This exploits Python’s object introspection capabilities to import the os module and execute system commands. Different template engines have different exploitation techniques, but the core principle remains the same: user input should never be directly embedded into templates as code.

Files et al.#

Local file inclusion#

We’ve seen an example of LFI in assignment 01, with a simple path traversal, but it can often look more interesting. For instance, in Flask, the static folder is by default served by the /static endpoint, even without any explicit configuration. So, if you decide to store user-uploaded content in static/uploads, any uploaded file will be available at /static/uploads/<filename>.

File Uploads#

As with fetching arbitrary files via LFI, the ability to upload arbitrary files to the server may open up avenues to many attacks.

For instance, servers with PHP often execute any code stored in a file with a .php extension. So, if you manage to upload a .php file and manage to access it, the server will most likely execute it as code.

A path traversal may allow attackers to overwrite system files and change functionality of the entire application.

As we’ve learned in the introduction to the web, files are uploaded in HTTP using the multipart/form-data content type, which allows users to specify both the filename, including an extension, and a MIME type. A server that wants to limit file formats which may be uploaded must check both of these, in addition to checking the contents of the file itself, depending on how these files will end up being used.

Server-Side Request Forgery#

Server-Side Request Forgery (SSRF) is a vulnerability that allows an attacker to make requests as the server to arbitrary destinations. This is particularly dangerous when the server often has access to internal services and resources that are not directly accessible from the internet. SSRF vulnerabilities commonly arise in features that fetch external URLs, do document/PDF generation, webhooks, or API integrations.

A simple example of SSRF might look like this:

@app.route('/fetch')
def fetch_url():
    url = request.args.get('url')
    response = requests.get(url)
    return response.text

Let’s say this would be present in an AWS cloud environment, an attacker can access metadata service at http://169.254.169.254/latest/meta-data/, which may expose sensitive information like IAM credentials, instance details, and security groups. There are equivalents for other clouds.

Cross-Protocol SSRF#

SSRF attacks can even be used to communicate with non-HTTP services by leveraging how different protocols interpret HTTP requests. For instance, you can send commands to a Redis server (an in-memory data store often used for caching and message queues) through an HTTP request by using the GET method combined with CRLF (Carriage Return Line Feed) injection:

GET /vuln.php?url=http://redis-server:6379/%0D%0ASET%20key%20value%0D%0A

The %0D%0A sequences are CRLF characters that allow injecting Redis protocol commands. While the Redis server will complain about the HTTP headers it doesn’t understand, it will still process the valid Redis commands embedded in the request. This technique can be used to write data to Redis, potentially leading to remote code execution.

Parsers et al.#

Insecure Deserialization#

Insecure deserialization occurs when an application accepts serialized objects from untrusted sources and deserializes them without proper validation. Serialization is the process of converting complex data structures or objects into a format that can be stored or transmitted, and deserialization reverses this process.

Not all serialization formats are equally dangerous. Text-based formats like JSON are generally safe because they only represent data structures without embedded code. However, binary serialization formats like Python’s pickle, PHP’s serialize(), Java’s serialized objects, and Ruby’s Marshal can embed executable code and are dangerous when deserializing untrusted data.

For example, Python’s pickle module, while convenient for serializing Python objects, can execute arbitrary code during deserialization:

import pickle
# Malicious serialized data
data = request.cookies.get('session')
user = pickle.loads(data)

Many programming languages and frameworks provide built-in serialization mechanisms, but these can be exploited if user-controlled data is deserialized without safeguards. Similar issues exist with PHP’s unserialize(), Java’s ObjectInputStream, and Ruby’s Marshal.load().

For instance, PHP’s unserialize() can be exploited to achieve remote code execution through magic methods like __destruct() or __wakeup() that automatically execute during deserialization. A real-world example is the Roundcube webmail vulnerability, where transformations on the serialized cookies resulted in a RCE.

YAML parsers in Python provide another interesting case. The PyYAML library has a yaml.load() function that, can execute arbitrary Python code embedded in YAML. For instance, a YAML payload like this can achieve remote code execution:

!!python/object/apply:os.system
args: ['id']

The secure approach is to use yaml.safe_load() instead, which only deserializes simple Python objects like strings, lists, and dictionaries. Similarly, for pickle, you should avoid deserializing untrusted data entirely, and for PHP, you should use JSON or other safe serialization formats instead of unserialize() when dealing with user input.

XML External Entities#

XML External Entities (or XXE) is about misusing features of XML parsers to exfiltrate files, or even achieve Remote Code Execution (RCE). Even though the usage of XML is slowly declining, it is still used in many, not only enterprise, applications to exchange data between programs. The original XML 1.0 standard defines the external entities mechanism, which allows parts of the document to be included from arbitrary URIs, including local files. The following code would disclose the contents of /etc/passwd in an insecurely configured parser:

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

So be careful to turn off these smart features when using XML parsers to parse untrusted data.

Race Conditions#

Since web servers often serve multiple requests at once, data created by one request might not be in a consistent state while another is being served. For instance, one thread may be receiving a file upload from a particularly slow client, while another tries to access this file and receives incomplete content.

Even when a variable/state in the server is shared across requests, it can cause trouble.

Take this code in Go taken from a challenge from the hxp 38C3 CTF:

err := os.Mkdir(dataDir, 0o777)
if err != nil {
	panic(err)
}

http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name")
	if err = checkPath(name); err != nil {
		http.Error(w, "checkPath :(", http.StatusInternalServerError)
		return
	}
	// ...
});

Because a : (which is used in Go to define a new variable) is missing in the err = checkPath(name); err != nil check, the err from the global scope is reused. It can be used to overwrite the err between the err = checkPath(name) assignment and the check err != nil with a different value (from a different request).

Standard inconsistencies#

HTTP Parameter Pollution#

As we briefly mentioned in the web intro, servers differ greatly in how they interpret when the same query parameter is specified multiple times. For instance, Flask always uses the first occurrence, while PHP uses the last. If you append [] to the end of a query parameter name, PHP then returns all values of such parameter as an array:

http://localhost:8080/?param[]=1&param[]=2
<?php echo $_GET["param"] // Returns an array of ["1", "2"]

If your code expects all query parameters to be strings, receiving an array might break things.

HTTP Request Smuggling#

HTTP Request Smuggling is a vulnerability that exploits inconsistencies in how different HTTP servers and proxies parse HTTP requests, particularly around the Content-Length and Transfer-Encoding headers. When a frontend proxy and backend server disagree about where one request ends and another begins, an attacker can “smuggle” a hidden second request that gets interpreted as coming from a different user.

The vulnerability typically arises when a request includes both Content-Length and Transfer-Encoding: chunked headers. Some servers prioritize Content-Length while others prioritize Transfer-Encoding, leading to desynchronization. For example, if a frontend proxy uses Content-Length to forward a request but the backend uses Transfer-Encoding, an attacker can craft a request where the backend sees additional data as the start of a new request.

Here’s a simple example of CL.TE (Content-Length vs Transfer-Encoding) smuggling:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED

Let’s consider the frontend proxy sees Content-Length: 13 and forwards exactly 13 bytes (including 0\r\n\r\n), thinking that’s the complete request. However, the backend server prioritizes Transfer-Encoding: chunked, interprets the 0 as the end of the chunked message, and treats SMUGGLED as the beginning of the next request in the TCP connection. This “smuggled” prefix can then be prepended to another user’s request, potentially hijacking their session or manipulating their request.

Modern mitigation involves ensuring consistent HTTP parsing across all infrastructure components, rejecting ambiguous requests that contain both headers, and using HTTP/2 which has a binary framing layer that prevents these ambiguities. Tools like Burp Suite’s HTTP Request Smuggler extension can help identify these vulnerabilities, and provides more on this topic.

TLDR#

  • Injection vulnerabilities occur when untrusted data is interpreted as code, they are quite common on the server:
    • SQL Injection (SQLi): Always use parameterized queries/prepared statements.
    • NoSQL Injection: Similar to SQLi but targets NoSQL databases like MongoDB. Attackers can inject query operators (e.g., $ne, $gt) through JSON payloads to bypass authentication or extract data.
    • Server-Side Template Injection (SSTI): User input becomes part of a template instead of just data. Attackers can inject template syntax (e.g., {{ 7*7 }}) and often escalate to RCE through object introspection. Use template engines correctly.
  • File-related vulnerabilities exploit how servers handle file operations:
    • Local File Inclusion (LFI): Path traversal attacks (../../../etc/passwd) allow reading arbitrary files. Always sanitize file paths.
    • File Upload attacks: Uploading malicious files (like .php shells) can lead to RCE. Validate filename, extension, MIME type, and actual file contents. Store uploads outside web root.
    • Server-Side Request Forgery (SSRF): Attacker makes the server send requests to internal services or cloud metadata endpoints (e.g., http://169.254.169.254/ on AWS). Can expose credentials and sensitive data. Validate and whitelist allowed destinations.
  • Parser vulnerabilities exploit how data formats are processed:
    • Insecure Deserialization: Deserializing untrusted data with formats like Python’s pickle, PHP’s serialize(), or Java’s serialized objects can execute arbitrary code. Use safe formats like JSON, or safe functions like yaml.safe_load().
    • XML External Entities (XXE): XML parsers may allow including external entities from arbitrary URIs, enabling file disclosure or RCE. Disable external entity processing in XML parsers.
  • Race Conditions: Multiple concurrent requests can create inconsistent state. Shared variables across requests or incomplete file uploads being accessed can cause security issues.
  • Standard inconsistencies in HTTP parsing create security gaps:
    • HTTP Request Smuggling: Exploits disagreements between frontend proxies and backend servers about Content-Length vs Transfer-Encoding headers. Attackers can “smuggle” requests that get prepended to other users’ requests, leading to session hijacking.

Further resources#