This page looks best with JavaScript enabled

Tìm hiểu về CVE-2021–3156 (A Sudo vulnerability)

 ·  ☕ 16 min read  ·  🐉 Edisc
Bài viết này nhằm mục đích ghi lại những kiến thức đã học để nhớ và hiểu rõ hơn.

Tổng quan

Lỗi của CVE này thuộc loại heap-based buffer overflow trong sudo. Cụ thể:

  • Nó cho phép người dùng bình thường lấy được root mà không cần biết mật khẩu của root.
  • Những phiên bản bị ảnh hưởng:
    • legacy có phiên bản của sudo từ 1.8.2 - 1.8.31p2
    • stable có phiên bản của sudo từ 1.9.0 - 1.9.5p1
  • Trong bài viết này, tôi tiến hành khai thác trên Ubuntu 18.04.5 LTS có phiên bản của sudo:
1
2
3
4
5
6
edisc@ubuntu:~$ sudo -V
Sudo version 1.9.5p1
Sudoers policy plugin version 1.9.5p1
Sudoers file grammar version 48
Sudoers I/O plugin version 1.9.5p1
Sudoers audit plugin version 1.9.5p1

Phân tích

Bức tranh sơ lược

  • Khi Sudo được thực thi ở chế độ command line:
    • Nếu sử dụng tùy chọn -s, thì Sudo's MODE_SHELL flag sẽ được bật.
    • Nếu sử dụng tùy chọn -i, thì Sudo's MODE_SHELL và MODE_LOGIN_SHELL flags sẽ được bật.
  • Khi thực thi, bắt đầu hàm main() của Sudo, hàm parse_args() sẽ viết lại argv (dòng 609-617) bằng cách nối tất cả các command-line arguments (dòng 587-595) và thêm vào trước các kí tự đặc biệt một dấu backslashes \. (dòng 590-591):
 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
571     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
572         char **av, *cmnd = NULL;
573         int ac = 1;
...
581             cmnd = dst = reallocarray(NULL, cmnd_size, 2);
...
587             for (av = argv; *av != NULL; av++) {
588                 for (src = *av; *src != '\0'; src++) {
589                     /* quote potential meta characters */
590                     if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
591                         *dst++ = '\\';
592                     *dst++ = *src;
593                 }
594                 *dst++ = ' ';
595             }
...
600             ac += 2; /* -c cmnd */
...
603         av = reallocarray(NULL, ac + 1, sizeof(char *));
...
609         av[0] = (char *)user_details.shell; /* plugin may override shell */
610         if (cmnd != NULL) {
611             av[1] = "-c";
612             av[2] = cmnd;
613         }
614         av[ac] = NULL;
615 
616         argv = av;
617         argc = ac;
618     }
  • Sau đó, trong hàm sudoers_policy_main(), hàm set_cmnd() nối tất cả các command-line arguments vào heap-based buffer "user_args" (dòng) 864-871) và bỏ qua kí tự \ dòng 866-867.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
852             for (size = 0, av = NewArgv + 1; *av; av++)
853                 size += strlen(*av) + 1;
854             if (size == 0 || (user_args = malloc(size)) == NULL) {
...
857             }
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
864                 for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
865                     while (*from) {
866                         if (from[0] == '\\' && !isspace((unsigned char)from[1]))
867                             from++;
868                         *to++ = *from++;
869                     }
870                     *to++ = ' ';
871                 }
...
884             }
...
886     }
  • Nếu command-line argument kết thúc với single backslash:
    • Tại dòng 866, from[0] = '\\'from[1]=NULL.
    • Tại dòng 867, from tăng 1 đơn vị, from trỏ tới NULL - from=NULL.
    • Tại dòng 868, NULL sẽ được copy vào user_args buffer, và from tăng 1 đơn vị, trỏ đến giá trị sau NULL (out of the argument’s bounds).
    • Vòng lặp while tại dòng 865-869 sẽ đọc và copy vùng dữ liệu out-of-bounds vào user_args buffer.
  • Dễ thấy, set_cmnd() là nguyên nhân dẫn tới lỗ hổng heap-based buffer overflow, bởi vì vùng dữ liệu out-of-bounds đã được copy vào user_args buffer, tuy nhiên nó lại không chỉ định kích thước của chúng (kích thước được tính toán tại dòng 852-853)
  • Về lí thuyết, không có command-line argument nào có thể kết thúc bằng dấu \ bởi vì:
    • Nếu MODE_SHELL hay MODE_LOGIN_SHELL được bật (line 858 - điều kiện cần để đến vùng code bị lỗi), thì MODE_SHELL được bật (dòng 571) và hàm parse_args() sẽ thêm kí tự \ vào các kí tự đặc biệt (meta-characters) bao gồm cả \ (nó sẽ thay \ thành \\)
  • Tuy nhiên, nếu quan sát kĩ, chúng ta sẽ thấy điều kiện gọi hàm của set_cmnd()parse_args() có một chút khác biệt:
1
2
3
819     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
858             if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

khác với

1
571     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
  • Câu hỏi đặt ra ở đây, liệu rằng ta có thể bật một trong ba mode MODE_EDIT, MODE_CHECK, MODE_SHELL mà không cần phải bật MODE_RUN - kích hoặc escape code không?
  • Câu trả lời hầu như không thể, bởi vì:
    • Nếu ta bật MODE_EDIT (tùy chọn -e, dòng 361) hay MODE_CHECK (tùy chọn -l, dòng 423 và 519), thì hàm parse_args() sẽ xóa MODE_SHELL từ valid_flags (dòng 363 và 424), trả về lỗi nếu tìm thấy invalid flag như MODE_SHELL (dòng 532-533):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
358                 case 'e':
...
361                     mode = MODE_EDIT;
362                     sudo_settings[ARG_SUDOEDIT].value = "true";
363                     valid_flags = MODE_NONINTERACTIVE;
364                     break;
...
416                 case 'l':
...
423                     mode = MODE_LIST;
424                     valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
425                     break;
...
518     if (argc > 0 && mode == MODE_LIST)
519         mode = MODE_CHECK;
...
532     if ((flags & valid_flags) != flags)
533         usage(1);
  • May mắn thay, chúng vẫn tồn tại một sơ hở:
    • Nếu chúng ta thực thi Sudo là sudoedit thay vì sudo, parse_args() sẽ tự động bật MODE_EDIT (dòng 270) nhưng không xóa các valid_flags, và MODE_SHELL lại nằm trong danh sách các valid_flags này (dòng 127 và 249):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
127 #define DEFAULT_VALID_FLAGS     (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
249     int valid_flags = DEFAULT_VALID_FLAGS;
...
267     proglen = strlen(progname);
268     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
269         progname = "sudoedit";
270         mode = MODE_EDIT;
271         sudo_settings[ARG_SUDOEDIT].value = "true";
272     }
  • Việc này dẫn đến: nếu chúng ta thực thi sudoedit -s, sau đó bật MODE_EDIT và MODE_SHELL nhưng không bật MODE_RUN, chúng ta sẽ tránh được escape code và đi đến vùng code bị lỗi. Sau đó, overflow heap-based buffer “user_args” thông qua command-line argument mà kết thúc bằng dấu backslash.
1
2
3
4
5
6
7
8
edisc@ubuntu:~$ sudo -s '\' `perl -e 'print "A" x 65536'`
[sudo] password for edisc: 
Sorry, try again.
[sudo] password for edisc: 
sudo: 1 incorrect password attempt
edisc@ubuntu:~$ sudoedit -s '\' `perl -e 'print "A" x 65536'`
Segmentation fault (core dumped)
edisc@ubuntu:~$
  • Dưới góc nhìn của kẻ tấn công, buffer overflow trong trường hợp này khá lí tưởng, bởi vì:
    • Chúng ta có thể kiểm soát được kích thước của vùng nhớ mà chúng ta overflow: user_args buffer (dòng 852-854)
    • Chúng ta hoàn toàn độc lập trong việc kiểm soát kích thước và nội dung của vùng nhớ bị overflow (dòng 852-853)
    • Chúng ta thậm chí có thể viết null bytes vào vùng nhớ bị overflow (tham số hoặc biến môi trường kết thúc bằng \ sẽ viết 1 null byte vào “user_args” dòng 866-868)
  • Ví dụ, trên amd64 Linux, chúng ta có thể cấp phát 24 bytes trong user-args buffer (32-heap chunk) và ghi đè những field của chunk tiếp theo với A=a\0B=b\0, ghi đè trường fd với C=c\0D=d\0 và trường bk với E=e\0F=f\0:
1
2
3
4
5
6
7
8
------------------------------------------------------------------------
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
------------------------------------------------------------------------

--|--------+--------+--------+--------|--------+--------+--------+--------+--
  |        |        |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
              size  <---- user_args buffer ---->  size      fd       bk
fd: forward
bk: backward
là hai con trỏ trong cấu trúc heap

Trace Heap Usages

  • Để hiểu được luồng thực thi của heap, chúng ta sẽ trace heap usage trên Ubuntu 18.04 từ malloc,realloc, calloc và hàm free với gdb script
 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
main()
|- setlocale(LC_ALL, "");
|- tzset()
|- sudo_conf_read_v1(CONF_DEBUG)
|   |- setlocale("C") // lưu vị trí hiện tại đầu tiên
|   |- read /etc/sudo.conf
|   |- setlocale(prev_locale) // khôi phục lại locale
|- sudo_conf_read_v1(CONF_ALL)  / giống với ở trên 
|- get_user_info()
|   |- getpwuid()
|   |   |- parse /etc/nsswitch.conf and create service_user structs
|- parse_args()
|- sudo_load_plugins()
|   |- load sudoers.so
|   |- register env hooks
|- policy_open()
|   |- format_plugin_settings()
|   |   |- initialize sudo settings such as network_addrs (many mallocs)
|   |- sudoers_policy_init()
|   |   |- init_defaults()
|   |   |   |- Nhiều chuỗi ngắn gọi strdup() (kể cả def_timestampdir)
|   |   |   |- một số chuỗi gọi dcgettext để cấp phát.
|   |   |   |- init_envtables() // tạo ra nhiều malloc() nhỏ
|   |   |- init_vars()
|   |   |- sudo_file_parse() load và parse /etc/sudoers
|- policy_check()
|   |- sudoers_policy_main()
|   |   |- set_cmnd()

Luồng thực thi từ một hàm xảy ra lỗi yêu cầu password.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
|- set_cmnd()   // lỗ hổng ở đây
|- setlocale("C")
|- sudo_file_lookup() // tìm kiếm dữ liệu được phân tích từ /etc/sudoers  
|   |- nss_*
|   |   |- nss_load_library()
|- setlocale(user_locale) // khôi phục lại locale
|- check_user()
|   |- check_user_interactive()
|   |   |- tạo tệp timestamp trong def_timestampdir
|   |   |- yêu cầu mật khẩu (nếu xác thực thất bại, thoát)  
|
|- find_editer()
|   |- call sudoer_hook_env("SUDO_EDITOR") // chỉ được gọi nếu xác thực thành công.
đã chạy gdbscript nhưng vẫn chưa note lại để kiểm tra xem cấu trúc có giống với tác giả chưa Đã làm được

Xác định những đối tượng có thể bị overwriting

  • Với luồng thực thi trên, có một số đối tượng (objects) được cấp phát trước khi chúng ta muốn tiến hành heap overflow và được sử dụng sau đó (lí tưởng cho việc ghi đè).
    1. nss service_user object
    2. def_timestampdir path
    3. compar function pointer in rbtree struct - Con trỏ hàm có thể bị ghi đè từng phần để bypass ASLRS (dùng một đoạn code bruteforcing), tuy nhiên tham số đầu tiên lại là chuỗi rỗng.
    4. userspects object from parsing /etc/sudoers: có khả năng bypass xác thực trên sudo có phiên bản >= 1.8.9 nhưng phải làm giả nhiều đối tượng (fake many objects).

glibc heap with/without tcache

  • Từ glibc phiên bản 2.25, tcache được thêm vào heap allocation.
  • So sánh giữa tcache bins và fast bins:
    • Giống:
      • Cả 2 đều đánh dấu đã được sử dụng.
      • Đều là LIFO (Last In, First Out).
      • Có thể cấp phát lại nếu kích thước yêu cầu cấp phát trùng với kích thước của bin.
    • Khác:
      • Max fast bín size là 0x80, còn max tache bins size là 0x410.
      • Khi kích thước yêu cầu cấp phát lớn hơn small bins (0x400), tất cả các fast bins được đưa vào unsorted bins, sau đó chúng được tập hợp lại, đưa vào bin nhỏ hơn hoặc lớn hơn. Có nhiều cấp phát yêu cầu khích thước lớn (như file buffer trong glibc) trong các chương trình chạy ở quyền sudo. Với tache bín, large chunk dược cấp phát sẽ không bị ảnh hưởng trong tcahe bins. Những free chunks với những kích thước nhất định sẽ nằm trong tache bin mãi.

Khai thác

glibc setlocale

  • Khi hàm setlocale được gọi với chuỗi rỗng, những biến môi trường LC_* được sử dụng như đầu vào cho hàm _nl_find_locale:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
115     if (cloc_name[0] == '\0')
116     {
117         /* The user decides which locale to use by setting environment 
118             variables*/
119         cloc_name = getenv("LC_ALL");
120         if (!name_present(cloc_name))
121             cloc_name = getenv(_nl_category_name.str 
122                                 + _nl_category_name_idxs[category]);
123         if (!name_present(cloc_name))
124             cloc_name = getenv("LANG");
125         if (!name_present(cloc_name))
126             cloc_name = _nl_C_name;
127     }
  • Đoạn code trên, ta thấy ưu tiên cho việc lấy locale name cho mỗi danh mục là LC_ALL, LC_<CATEGORY_NAME>, LANG environment. Nếu không có gì được thiết lập, special locale name "C" được sử dụng. Nếu locale name là “C”, hàm _nl_find_locale sẽ trả về ngay lập tức mà không đi tới heap, với những tên khác, flow sẽ như sau:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
185     /*LOCALE can consist of up to four recognized parts for the XPG syntax:
186             language[_territory[.codeset]][@modifier]
187     
188         Beside the first all of them are allowed to be missing. If the full specified locale is not found, the less specific one are looked for. The various part will be stripped off according to the following order:
189             (1) codeset
190             (2) normalized codeset
191             (3) territory
192             (4) modifier        
*/
mask = _nl_explode_name (loc_name, &language, &modifier, &territory, 
                        &codeset, &normalized_codeset);

Giá trị trả về mark là cờ để chỉ ra sự tồn tại của các phần này trong locale name được nhận. Sau đó hàm _nl_make_l10nflist được gọi để kiểm tra liệu locale name đã nhận có được tải lên hay chưa? Điều này yêu cầu malloc để lưu đầy đủ tên thư mục. Nếu nó nằm trong danh sách/lệnh gọi chỉ để nhận, giải phóng nó rồi trả về.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
165     /*Allocate room for the full file name. */
166     abs_filename = (char *) malloc(dirlist_len
167                                     + strlen(language)
168                                     + ((mask & XPG_TERRITORY) != 0
169                                         ? strlen (codeset) + 1 : 0)
170                                     + ((mask & XPG_CODESET) != 0
171                                         ? strlen(normalized_codeset) + 1 : 0)
172                                     + ((mask & XPG_NORM_CODESET) != 0
173                                         ? strlen(normalized_codeset) + 1 : 0)
174                                     + ((mask & XPG_MODIFIER) != 0
175                                         ? strlen(modifier) + 1 : 0)
176                                     + 1 + strlen(filename) +1);
  • Nếu locale name không nằm trong danh sách được load, hàm _nl_make_l10nflist được gọi một lần nữa để tạo ra tất cả các đường dẫn có thể (đường dẫn cơ bản là /usr/lib/locale) từ việc kết hợp những thành phần và đặc tính của tên bằng gọi đệ quy chính đó với “mask” đã được sửa đổi. Thuật toán này sẽ tạo ra một duplicated part (malloc) và rồi xóa nó (free) sau khi kiểm tra xong. Càng nhiều thành phần trong locales thì sẽ có càng nhiều hàm malloc và free được gọi.
  • Sau đó, hàm _nl_find_locale cố gắng load locale data từ từng đường dẫn một. Nếu tìm thấy một locale data hợp lệ, hàm setlocale sẽ sawpx xếp tên miền nhất định và lưu nó trong nội bộ.
  • Nếu có lỗi xảy ra, hàm setlocale sẽ giải phóng tất cả các tên đã được lưu và mặc định sử dụng C rồi trả về ngay lâp tức. Do đó, locale name không thể ngẫu nhiên, ít nhất ngôn ngữ và bộ mã phải hợp lệ.
  • Sau khi tất cả tên danh mục đều được strdup() và dữ liệu được load, LC_ALL sẽ được tạo trong hàm new_composite_name. Nếu tất cả các tên LC giống nhau, giá trị của nó sẽ chỉ được lấy từ tên đầu tiên, ngược lại, nếu không giống, giá trị sẽ được kết hợp lại với nhau.

Kiểm soát heap usage bằng biến môi trường.

Một số giá trị của biến môi trường giúp cho việc khai thác lỗi này:

  • Thiết lập môi trường "TZ=:": giảm số lần sử dụng heap trong hàm glibc tzset() và hoàn toàn có thể tiên đoán trước.
  • Thêm ";x=x" trong mọi biến môi trường LC, luồng thực thi sẽ như sau:
    • Đầu tiên, hàm setlocale("") sẽ cấp phát và giải phóng một cách bình thường, LC_ALL sẽ có giá trị "...;x=x;..."
    • Sau đó, setlocale(NULL) lấy giá trị LC_ALL hiện tại và lưu lại.
    • setlocale("C") sẽ giải phóng tất cả locale name nhận được.
    • setlocale(saved_LC_ALL) sẽ không làm gì, vì x là tên danh mục không hợp lệ.
    • Tới đây, LC_ALL trong glibc là “C”
    • setlocale sẽ không làm gì bởi vì LC_ALLC
    • Kết quả chúng ta sẽ có một vùng được giải phóng với kích thước được kiểm soát bằng cách thiết lập môi trường LC

Cấu trúc service_user

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
typedef struct service_user
{
    /* And the link to the next entry.  */
    struct service_user *next;
    /* Action according to result.  */
    lookup_actions actions[5];
    /* Link to the underlying library object.  */
    service_library *library;
    /* Collection of known functions.  */
    void *known;
    /* Name of the service (`files', `dns', `nis', ...).  */
    char name[0];
} service_user;

-Cấu trúc này được sử dụng trong nss_load_library của libc khá thường xuyên khi vấn đề overflow xảy ra (trace log) để load những thư viện liên kết động lên. Chúng ta có thể ghi đè trường name và tải thư viện của chúng ta lên.

  • Sau đó, chúng ta nhắm tới những thư viện mà không có đặc quyền, chạy nó với quyền root.
  • Hàm như sau:
 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
static int
nss_load_library (service_user *ni)
{
    if (ni->library == NULL)
        {
        static name_database default_table;
        ni->library = nss_new_service (service_table ?: &default_table,
                        ni->name);
        if (ni->library == NULL)
        return -1;
        }

    if (ni->library->lib_handle == NULL)
        {
        /* Load the shared library.  */
        size_t shlen = (7 + strlen (ni->name) + 3
                + strlen (__nss_shlib_revision) + 1);
        int saved_errno = errno;
        char shlib_name[shlen];

        /* Construct shared object name.  */
        __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
                            "libnss_"),
                        ni->name),
                ".so"),
            __nss_shlib_revision);

        ni->library->lib_handle = __libc_dlopen (shlib_name);
  • Mục tiêu của hàm này là đến ni->library->lib_handle = __libc_dlopen(shlib_name) để tải thư viện mà chúng ta kiểm soát.
  • Có hai điều mà chúng ta cần lưu ý:
    • Nếu ni->library khác NULL, chúng ta sẽ sử dụng con trỏ trong ni->library->lib_handle và vì ASLR nên chúng ta không thể đoán liệu rằng con trỏ này có hợp lệ hay không. Chúng ta thiết lập ni->library=nss_new_service(...) nếu con trỏ này NULL, sau đó chúng ta chỉ cần ghi đè cấu trúc này để lấy được tên trường và đổi nó tới những thư viện mà chúng ta kiểm soát.
    • Thách thức thứ 2 là chúng ta có con trỏ struct service_user *next. Nếu chúng ta gây ra overflow

Ghi đè cấu trúc service_user bằng glibc tcache

Từ trace log, chúng ta thấy 2 lệnh gọi nss_load_library. Sau đó ta kiểm tra xem service_user object được tạo ở vị trí nào

  • Nó được tạo từ “nhóm” các dòng trong file “nsswitch.conf”. Như chúng ta thấy, sudo sẽ tạo ra service_user objects (từ 2 dịch vụ trong passwd line). Do đó, mã thực thi của chúng ta sẽ đọc file nsswitch.conf đẻ xác định offset hoặc số lượng chunk size = 0x40 được tạo.
  • Thiết lập TZ=:;x=x trong môi trường LC sẽ giúp chúng ta điều khiển việc sử dụng heap trước khi phân tích file "/etc/nsswitch.conf"
  • Trace hàm malloc/free từ lệnh gọi hàm sudo_conf_read_v1 tới lệnh get_user_info ta sẽ được như dưới
    làm sao để trace được như tác giả
  • Ví dụ, ta thiết lập môi trường "LC_CTYPE=C.UTF-8@" + "A"*0x28"LC_NUMERIC=C.UTF-8@"+"A"*0x86. Chunk size của LC_CTYPE0x30+8 (8 là kích thước của heap metadata), sau đó được làm tròn lên 0x40 và chunk size của LC_NUMERIC là 0x80. Rồi chúng ta sẽ được bố cục tượng tự như hình bên cho nsswitch.conf với 2 dịch vụ cho passwd.
1
2
3
4
    LC_COLLATE          LC_TIME         LC_NUMERIC          LC_CTYPE
--+-------------+----+------------+---+-------------+---+-------------+---
  | free 0x40   | .. | free 0x40  |...|  free 0x80  |...|  freed 0x40 |
--+-------------+----+------------+---+-------------+---+-------------+---

Do đó, sẽ hướng đến servirce_user object tại vùng nhớ LC_CTYPE bị giải phóng và thực hiện heap overflow tại LC_NUMERIC để ghi đè service_user object

Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page