본문 바로가기
hacking & security/web

LINE CTF - 2021 ( diveInternal, your-note )

by 탁종민 2021. 3. 23.

challenge code:

https://github.com/zbvs/ctf/tree/master/linectf-2020

 

- diveInternal

 

- docker-compose.yml

version: "2"
services:
    nginx:
        build:
            context: ./nginx/
        container_name: linectf_diveinternal_nginx
        restart: always  
        image: linectf_diveinternal_nginx
        ports:
            - "12004:80"
        networks:
            - ctf-network
    public:
        build:
            context: ./public/
            target: production 
        container_name: linectf_diveinternal_public
        restart: always  
        image: linectf_diveinternal_public
        networks:
            - ctf-network
    redis:
        image: "redis:alpine"
        restart: always 
        container_name: linectf_diveinternal_redis
        networks:
            - ctf-network
    private:
        build: ./private/
        container_name: linectf_diveinternal_private
        restart: always 
        image: linectf_diveinternal_private
        networks:
            - ctf-network
        environment:
            - FLASK_APP=main.py
            - 'RUN=flask run --host=0.0.0.0 --port=5000'
            - ENV=PROD
            - FLASK_DEBUG=0
            - CHKTIME=6
            - DBFILE=database/master.db
networks:
   ctf-network:

포트맵핑이 nginx로 되어있고 nginx는 public 서비스로 패킷을 포워딩 한다. 그래서 private은 직접 접근할 수 없고 public을 통해서 접근할 수 있다. public은 express 서버고 private은 flask 서버다. 

 

- public/src/routes/apis.js

router.get('/coin', function(req, res, next) {
  request({
        headers: req.headers,
        uri: `http://${target}/coin`,
      }).pipe(res);
  });

 

http://public/apis/coin 경로에 패킷을 보내면 public은 http://private/coin으로 헤더를 그대로 넘겨서 접근한다.

 

- private/main.py

 def LanguageNomarize(request):
    if request.headers.get('Lang') is None:
        return "en"
    else:
        regex = '^[!@#$\\/.].*/.*' # Easy~~
        language = request.headers.get('Lang')
        language = re.sub(r'%00|%0d|%0a|[!@#$^]|\.\./', '', language)
        if re.search(regex,language):
            return request.headers.get('Lang')
        
        try:
            data = requests.get(request.host_url+language, headers=request.headers)
 
 ...
 
@app.route('/coin', methods=['GET'])
def coin():
    try:
        response = app.response_class()
        language = LanguageNomarize(request)
        response.headers["Lang"] =  language
 
 
 ...
 

 

private/main.py 에서 coin GET 핸들러는 Lang HTTP 헤더가 있으면 http://private/language 로 패킷을 보내 응답을 받아 재설정한다. 여기서 Lang 헤더를 이용해 SSRF를 할 수 있다.

 

- private/main.py

...
privateKey = b'let\'sbitcorinparty'

class Activity():
...
    def IntegrityCheck(self,key, dbHash): 

        if self.integrityKey == key:
            pass
        else:
            return json.dumps(status['key'])
        if self.dbHash != dbHash:
            flag = RunRollbackDB(dbHash)

...

 def SignCheck(request):
    sigining = hmac.new( privateKey , request.query_string, hashlib.sha512 )

    if sigining.hexdigest() != request.headers.get('Sign'):
        return False
    else:
        return True
        
...
 
@app.route('/rollback', methods=['GET'])
def rollback():
    try:
            ...
            
            if SignCheck(request):
                pass
            ...
        result  = activity.IntegrityCheck(request.headers.get('Key'),request.args.get('dbhash'))
 

        

SSRF로 http://private/rollback 경로가 플래그를 얻을 수 있는 경로인데 몇 가지 작업이 필요했다.

첫번째는 signing 값을 하드코딩된 privatekey 와 query_string 의 sha512 값으로 맞춰줘야한다.

두번째는 integrity key 값을 맞춰야 하는데 integrity key 값은 sha512( dbhash ) 값이고 dbhash 값은 SSRF로 http://private/integrityStatus 경로에 접근하면 얻을 수 있다.

세번째는 backup dbhash 파일이름을 알아 맞춰야하는데 SSRF로 http://private/download api를 이용하면 지정된 이름으로 backup file을 upload 할 수 있어서 hash값을 맞출 필요 없이 그대로 bypass 할 수 있다.

 

 

LINECTF{YOUNGCHAYOUNGCHABITCOINADAMYMONEYISBURNING}

 

 

- your-note

 

- docker-compose.yml

version: '3'

services:
  yournote-web:
    restart: always
    build: web
    container_name: yournote-web
    ports:
      - '80:80'
    env_file: '.env'
    depends_on:
      - yournote-db

  yournote-db:
    image: mysql:5.7
    container_name: yournote-db
    env_file: '.env'
    volumes:
      - './db/data:/var/lib/mysql'

  # yournote-admin:
  #   restart: always
  #   build: crawler
  #   container_name: yournote-admin
  #   env_file: '.env'
  #   depends_on: 
  #     - yournote-web

flask 랑 puppeteer admin ( crawler.js ) 으로 구성된 문제다. 

 

- yournote-web/app.py

...

@app.route('/search')
@login_required
def search():
    q = request.args.get('q')
    download = request.args.get('download') is not None
    if q:
        notes = Note.query.filter_by(owner=current_user).filter(or_(Note.title.like(f'%{q}%'), Note.content.like(f'%{q}%'))).all()
        if notes and download:
            return Response(json.dumps(NoteSchema(many=True).dump(notes)), headers={'Content-disposition': 'attachment;filename=result.json'})
    else:
        return redirect(url_for('index'))
    return render_template('index.html', notes=notes, is_search=True)

...

@app.route('/report', methods=['GET', 'POST'])
@login_required
def report():
    if request.method == 'POST':
        url = request.form.get('url')
        proof = request.form.get('proof')
        if url and proof:
            res = requests.get(
                app.config.get('CRAWLER_URL'),
                params={
                    'url': url,
                    'proof': proof,
                    'prefix': session.pop('pow_prefix')
                }
            )
    ...

yournote-web/report 에 POST로 url이랑 proof-of-work의 정답을 제공하면 flask가 crawler 에 GET을 보내 cralwer를 작동시킨다. 

만약 crawler 에게 yournote-web/search?q=LINECTF{&download 형태로 flag의 substring을 GET 쿼리로 보내면 crawler는 페이지를 방문하고 download를 하다가 error를 낸다. 그래서 flag의 substring을 제대로 보냈을 때의 반응 차이를 이용해 sql 공격이 가능하다. 한번 report를 보낼 때마다 pow를 계산해야 하지만 스레드 어떻게 잘 만들어서 프로그래밍 하면 30분~1시간 내로 추출해내겠다 싶어서 그대로 풀었다. 하지만 네트워크 상황이 서버가 너무 자주 다운되어서 굉장히 까다로웠고 오래걸렸다.

 

LINECTF{1-kn0w-what-y0u-d0wn10ad}

 

나중에 다른 Writeup에서 찾아본 제대로된 풀이는 Chrome옵션 "--disable-popup-blocking" 으로 인한 Cross-Origin-Opener-Policy 정책이 꺼지는 걸 이용해 푸는거라고 한다.

 

 

 

 

'hacking & security > web' 카테고리의 다른 글

pbctf 2020 - sploosh writeup  (0) 2020.12.17

댓글