RWX - Bronze
Solution
Challenge cho mình một trang web với 3 chức năng là đọc, ghi file và thực hiện lệnh linux nhưng chỉ giới hạn 7 char.
from flask import Flask, request, send_file
import subprocess
app = Flask(__name__)
@app.route('/read')
def read():
filename = request.args.get('filename', '')
try:
return send_file(filename)
except Exception as e:
return str(e), 400
@app.route('/write', methods=['POST'])
def write():
filename = request.args.get('filename', '')
content = request.get_data()
try:
with open(filename, 'wb') as f:
f.write(content)
return 'OK'
except Exception as e:
return str(e), 400
@app.route('/exec')
def execute():
cmd = request.args.get('cmd', '')
if len(cmd) > 7:
return 'Command too long', 400
try:
output = subprocess.check_output(cmd, shell=True)
return output
except Exception as e:
return str(e), 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=6664)
Ở đây flag được đưa vào thư mục root và cấp quyền chỉ admin mới đọc được.
WORKDIR /
COPY flag.txt /
RUN chmod 400 /flag.txt
COPY would.c /
RUN gcc -o would would.c && \
chmod 6111 would && \
rm would.c
Ngoài ra challenge còn cho ta một file read flag như sau yêu cầu phải có argument là một chuỗi “you be so kind to provide me with a flag”
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
char full_cmd[256] = {0};
for (int i = 1; i < argc; i++) {
strncat(full_cmd, argv[i], sizeof(full_cmd) - strlen(full_cmd) - 1);
if (i < argc - 1) strncat(full_cmd, " ", sizeof(full_cmd) - strlen(full_cmd) - 1);
}
if (strstr(full_cmd, "you be so kind to provide me with a flag")) {
FILE *flag = fopen("/flag.txt", "r");
if (flag) {
char buffer[1024];
while (fgets(buffer, sizeof(buffer), flag)) {
printf("%s", buffer);
}
fclose(flag);
return 0;
}
}
printf("Invalid usage: %s\n", full_cmd);
return 1;
}
Vì thế để đọc được flag ta phải thực hiện lệnh sau /would you be so kind to provide me with a flag
nhưng lại không thỏa yêu cầu tối đa 7 char. Lúc này mình sẽ sử dụng endpoint /write tạo ra một shell script và chạy lệnh sh ~/a
chỉ có 6 char nên sẽ bypass thành công. Đầu tiên ta xác định vị trí thư mục ~ là ở /home/user
Tiến hành ghi vào file shell lệnh read flag trên
Kiểm tra nội dung file shell và thấy rằng lệnh shell đã ghi vào thành công
Bây giờ chỉ cần thực thi shell script bằng sh là xong
kalmar{ok_you_demonstrated_your_rwx_abilities_but_let_us_put_you_to_the_test_for_real_now}
RWX - Silver
Solution
Bài này giông với bài Bronze nhưng /exec chỉ giới hạn lệnh tối đa 5 char.
from flask import Flask, request, send_file
import subprocess
app = Flask(__name__)
@app.route('/read')
def read():
filename = request.args.get('filename', '')
try:
return send_file(filename)
except Exception as e:
return str(e), 400
@app.route('/write', methods=['POST'])
def write():
filename = request.args.get('filename', '')
content = request.get_data()
try:
with open(filename, 'wb') as f:
f.write(content)
return 'OK'
except Exception as e:
return str(e), 400
@app.route('/exec')
def execute():
cmd = request.args.get('cmd', '')
if len(cmd) > 5:
return 'Command too long', 400
try:
output = subprocess.check_output(cmd, shell=True)
return output
except Exception as e:
return str(e), 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=6664)
Ở đây chúng ta cần biết rằng .
là một shell builtin tương đương với lệnh source của linux
Từ đó mình sẽ thực hiện như bài Bronze nhưng ở bước cuối mình sẽ thực hiện lệnh sau . ~/a
để bypass
kalmar{impressive_that_you_managed_to_get_this_far_but_surely_silver_is_where_your_rwx_adventure_ends_b4284b024113}
babyKalmarCTF
Solution
Challenge cho mình một trang web CTF như sau
Ở đây web sẽ có các challenge impossible với tổng điểm 4000 và các challenge dễ hơn.
Để lấy được flag chúng ta phải lấy được top 1 của web này.
Lúc này mình đọc source của hàm get_score mà trang web này dùng thì thấy rằng hàm tính value đã bị đổi và nhận vào team_count là maxSolves tức càng nhiều team thì điểm challenge càng cao.
Ok thì mình đã xác định được hướng giải là tạo thật nhiều account để buff điểm các bài dễ lên, với số lượng là 5 bài và hơn 900 điểm mỗi bài thì ta chắc chắn sẽ có top 1. Có 1 vấn đề là khi register nó đã dính CSP để chống CSRF nên ta không thể spam request được.
Lúc này mình chỉ cần sử dụng webdriver để tạo là được. Đây là script để tạo 100 user bằng selenium.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import string
import random
import time
browser = webdriver.Firefox()
browser.get('https://957d8491456aacec91258c75e9e18bb8-60507.inst1.chal-kalmarc.tf/register')
def id_generator(size=10, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
for i in range(100):
name = id_generator()
email = name + '@' + name + '.' + name
print(name, email)
elem1 = browser.find_element(By.NAME, 'name')
elem1.send_keys(name)
elem2 = browser.find_element(By.NAME, 'email')
elem2.send_keys(email)
elem3 = browser.find_element(By.NAME, 'password')
elem3.send_keys(email)
elem4 = browser.find_element(By.NAME, '_submit')
elem4.click()
elem5 = browser.find_element(By.XPATH, '/html/body/main/div[2]/div[2]/div[2]/a')
elem5.click()
elem6 = browser.find_element(By.NAME, 'name')
elem6.send_keys(name)
elem7 = browser.find_element(By.NAME, 'password')
elem7.send_keys(email)
elem8 = browser.find_element(By.NAME, '_submit')
elem8.click()
elem11 = browser.find_element(By.XPATH, '/html/body/nav/div/div/ul[2]/li[5]/a/span[2]/i')
elem11.click()
elem12 = browser.find_element(By.XPATH, '/html/body/nav/div/div/ul[2]/li[1]/a')
elem12.click()
Ok trong lúc đợi tạo user thì ta sẽ bắt đầu giải các challenge. Đầu tiên là một bài crypto về RSA.
Challenge cho ta một file script và output như sau
from Crypto.Util.number import getPrime
with open("flag.txt", "rb") as f:
flag = f.read()
flag = int.from_bytes(flag, 'big')
e = 65537
p,q,r = [getPrime(512) for _ in "pqr"]
print(f'n1 = {p*q}')
print(f'c1 = {pow(flag, e, p*q)}')
print(f'n2 = {q*r}')
print(f'c2 = {pow(flag, e, q*r)}')
print(f'n3 = {r*p}')
print(f'c3 = {pow(flag, e, r*p)}')
n1 = 92045071469462918382808444819504749563961839349096597384482544087908047186245341810642171828493439415203636331750819922984117530107215197072782880474039650967711411408034481971170502798025943494586125686145145275611434604037182033168196599652119558449773401870500131970644786235514317736653798125756404891127
c1 = 83837022114533675382122799116377123399567305874353525217531313052347013266429457590484976944405567987615711918756165213164809141929523845319047846779529628627662566542055574929528850262048285117600900265045865263948170688845876052722196561247534915037323009007843324908963180407442831108561689170430284682827
n2 = 138872353325175299307460237192549876070806082965466021111327520189900415231224864814489473847190673904249096844311163666118481717154197936898625500598207447786178788728989474031735348581801399821380599701957041743964351118199095341359179067904834006929292304447601473687076874217599854120530320878903822568483
c2 = 108277854219556753624555311292632391078510528708411323024976641264748291782337772568140557355433905939549254699367886423180057883496836376992252188314404115061609464533109517754775889103063279929956348746519414221014574988017949824063805698193300538273109123053143777891136649709207700596337731172498156528258
n3 = 96873643524161216047523283610645732806192956944624208819078561364455621631633510067022852244593247313195537163455457833157440906743895116798782534912117642844197952559448815829606193149605373700004399064513744456542191695589096233791113561406431990041145854326610075794048654641871205275800952496149515217589
c3 = 87497536561550257160428999520415606634926951187670727897152089479182062251287235760026406551482417341218358001218344037520058606273067256839313353071151191482530927154606346622780052423032142990543077247694313298271089760031393294084220768215879358822723955182536249471261313038497315002109953940648304272403
Vì bài này khá dễ nên mình có thể xây dựng solve script như sau
from Crypto.Util.number import long_to_bytes, inverse, GCD
n1 = 92045071469462918382808444819504749563961839349096597384482544087908047186245341810642171828493439415203636331750819922984117530107215197072782880474039650967711411408034481971170502798025943494586125686145145275611434604037182033168196599652119558449773401870500131970644786235514317736653798125756404891127
c1 = 83837022114533675382122799116377123399567305874353525217531313052347013266429457590484976944405567987615711918756165213164809141929523845319047846779529628627662566542055574929528850262048285117600900265045865263948170688845876052722196561247534915037323009007843324908963180407442831108561689170430284682827
n2 = 138872353325175299307460237192549876070806082965466021111327520189900415231224864814489473847190673904249096844311163666118481717154197936898625500598207447786178788728989474031735348581801399821380599701957041743964351118199095341359179067904834006929292304447601473687076874217599854120530320878903822568483
c2 = 108277854219556753624555311292632391078510528708411323024976641264748291782337772568140557355433905939549254699367886423180057883496836376992252188314404115061609464533109517754775889103063279929956348746519414221014574988017949824063805698193300538273109123053143777891136649709207700596337731172498156528258
n3 = 96873643524161216047523283610645732806192956944624208819078561364455621631633510067022852244593247313195537163455457833157440906743895116798782534912117642844197952559448815829606193149605373700004399064513744456542191695589096233791113561406431990041145854326610075794048654641871205275800952496149515217589
c3 = 87497536561550257160428999520415606634926951187670727897152089479182062251287235760026406551482417341218358001218344037520058606273067256839313353071151191482530927154606346622780052423032142990543077247694313298271089760031393294084220768215879358822723955182536249471261313038497315002109953940648304272403
e = 65537
q = GCD(n1, n2)
r = GCD(n2, n3)
p = GCD(n3, n1)
phi_n1 = (p - 1) * (q - 1)
d = inverse(e, phi_n1)
m = pow(c1, d, n1)
flag = long_to_bytes(m).decode()
print(flag)
Và ta đã có flag đầu tiên
babykalmar{wow_you_are_an_rsa_master!!!!!}
Ở bài rev này mình bật ghidra lên đọc và có luôn flag
babykalmar{string_compare_rev_ayoooooooo}
Challenge cho mình một bức ảnh
Sử dụng google lens và mình có luôn city của chỗ này là Aarhus
babykalmar{aarhus}
Challenge cho ta các một đoạn chữ Braille
⠃⠁⠃⠽⠅⠁⠇⠍⠁⠗{⠎⠥⠏⠑⠗⠕⠗⠊⠛⠊⠝⠁⠇⠍⠕⠗⠎⠑⠉⠕⠙⠑⠉⠓⠁⠇⠇⠑⠝⠛⠑}
Sử dụng tool decode online và mình cos được flag
babykalmar{superoriginalmorsecodechallenge}
Challenge welcome này đã tự cho ta flag
babykalmar{welcome_to_babykalmar_CTF}
Tiếp theo ta submit cả 5 bài và đều có điểm trên 900
Cộng lại và mình đã top 1 server
Bây giờ chỉ việc lấy flag thôi
Flag: kalmar{w0w_y0u_b34t_k4lm4r_1n_4_c7f?!?}
Ez ⛳ v3
Hints
Host header attack, SSRF and SSTI
Solution
Challenge cho ta một web server sử dụng Caddy
{
debug
servers {
strict_sni_host insecure_off
}
}
*.caddy.chal-kalmarc.tf {
tls internal
redir public.caddy.chal-kalmarc.tf
}
public.caddy.chal-kalmarc.tf {
tls internal
respond "PUBLIC LANDING PAGE. NO FUN HERE."
}
private.caddy.chal-kalmarc.tf {
# Only admin with local mTLS cert can access
tls internal {
client_auth {
mode require_and_verify
trust_pool pki_root {
authority local
}
}
}
# ... and you need to be on the server to get the flag
route /flag {
@denied1 not remote_ip 127.0.0.1
respond @denied1 "No ..."
# To be really really sure nobody gets the flag
@denied2 `1 == 1`
respond @denied2 "Would be too easy, right?"
# Okay, you can have the flag:
respond {$FLAG}
}
templates
respond /cat `{{ cat "HELLO" "WORLD" }}`
respond /fetch/* `{{ httpInclude "/{http.request.orig_uri.path.1}" }}`
respond /headers `{{ .Req.Header | mustToPrettyJson }}`
respond /ip `{{ .ClientIP }}`
respond /whoami `{http.auth.user.id}`
respond "UNKNOWN ACTION"
}
Phân tích:
Mục tiêu của ta là vào private.caddy.chal-kalmarc.tf để lấy flag
Nhưng khi vào các subdomain có dạng
*.caddy.chal-kalmarc.tf
thì đều redirect về public.caddy.chal-kalmarc.tf nên ta không thể bypass đượcỞ đây ta sẽ quan tâm
strict_sni_host insecure_off
là một option cho phép ta sử dụng Host header khong cần match với url tức là chúng ta có thể sử dụng url từ nguồn khác.
Ở đầy mình thay đổi host thành public.caddy.chal-kalmarc.tf và có được respond như trong file chứ không còn redirect nữa
Ok đến đây thì mình chỉ cần vào endpoint /flag của private.caddy.chal-kalmarc.tf
Nhưng không web đã giới hạn ip gửi request phải là từ localhost túc là từ chính máy đang chạy server
@denied1 not remote_ip 127.0.0.1
respond @denied1 "No ..."
Ta quan sát kỹ lại thì thấy có endpoint /fetch dùng để lấy thông tin từ endpoint khác và không bị giới hạn
respond /fetch/* `{{ httpInclude "/{http.request.orig_uri.path.1}" }}`
Lúc này mình sẽ sử dụng /fetch/flag để lấy nội dung từ /flag và đã thành công
Nhưng nó lại không trả ra flag vì bị chặn bởi
@denied2 `1 == 1`
respond @denied2 "Would be too easy, right?"
Ok lúc này thì hết cách bypass rồi chúng ta quan sát và thấy /headers đùng để trả ra các header mà request của mình gửi tới server
Lúc này mình mới research và biết rằng có template có thể lấy biến từ environment mà flag thì nằm trong đó. https://caddyserver.com/docs/modules/http.handlers.templates
Craft payload vào header và ta đã thực hiện SSTI thành công.
FLag: kalmar{4n0th3r_K4lmarCTF_An0Th3R_C4ddy_Ch4ll}
KalmarNotes
Updating …