本次题目的地址为sepolia@0x053cd080A26CB03d5E6d2956CeBB31c56E7660CA
这一次1024程序员节中有区块链相关的题目,作为今年才开始起步区块链的小萌新,这一题也是整整看了一整个周末才做出来,不过做出来之后也是相当的具有成就感滴:),话不多说,我们现在就来看一看如何做出这一题.
先上合约源码↓↓↓↓↓↓
这里我们注意到了一个函数payforflag
,很明显,我们需要调用这一个函数来获得我们的flag,那么调用这个函数的条件是什么呢?
我们需要_flag[msg.sender]
的值为2
接下来要做的就是寻找函数使_flag[msg.sender]
的值到2.
通过寻找,我们找到了withdraw
这个函数,而这个函数的执行需要满足两个条件,分别是ethbalances[msg.sender] >= 1
和_ebalances[msg.sender] >= 1812
.
先看第一个条件ethbalances[msg.sender] >= 1
,我们可以使用deposit
这个函数来令其满足
再看第二个条件_ebalances[msg.sender] >= 1812
,涉及到该变量的函数有profit
,borrow
,buy
,sale
我们看看profit
这个函数,只能运行一次,获得一个_balances
;而borrow
这个函数,一共可以执行两次获得两个_balances
.但是这两个函数都有_profited[msg.sender]
这个变量进行限制,也就是说,我们最多只能通过profit
或borrow
函数获得2个_balances
.
那么_balances
有什么用呢?看一看sale
函数,我们可以把_balances
卖掉得到_ebalances
,其中tokenprice
已经被定义为6了,所以_balances
与_ebalances
之间的兑换比例为1:6.
而buy
这个函数,只有当_ebalances
大于233时,_ebalances
与_balances
之间的兑换比例才是1:1.
仔细看看上面两段话,稍微思考一下就可以明白,只要我的_ebalances
比233要大,那么不就可以通过与_balances
互刷的方式不断增加我的_ebalances
从而满足条件2_ebalances[msg.sender] >= 1812
?!
这里我举个简单的例子,假设我现在有_ebalances
300个,那么我可以通过buy(300)
获得_balances
300个,随后在通过sale(300)
获得_ebalances
300*6=1800个,然后再重复上面的过程,那么我的_ebalances
不久可以源源不断的增加的吗~~~~
所以我们现在要做的可以是:
在求解这一题的过程中,我想到了两种方法都可以来获得flag,接下来听我一一道来~~
我们知道每一个初始账号都可以固定获得2个_balances
,那么我们能否通过小号为大号通过transfer
方法发送_balances
的方法获得足够数量的_balances
呢?答案是可行的.
直接上代码!
先写一个拿两个_balances
并转给大号的合约
再写一个批量创建合约的合约
通过调用第二个合约,给大号足够的_balances
启动资金,就可以开始刷_ebalances
拿flag咯~~
细心的同学在做这题的时候有没有发现这个函数flashloan
,可以直接给你增加300的_ebalances
!
不过直接编写攻击合约来调用这个函数肯定是不行滴,因为require(msg.sender == address(this), "hacker get out")
这一句的限制了,咋办嘞?
再找找看叭~~于是我们找到了一个调用flashloan
的函数testborrowtwice
,这不就正好可以满足上面的条件了吗~
不过flashloan
内还有限制条件require(_authd[scoupon.coupon.buser] == 2, "need pre auth")
,就是说需要验证的意思,我们找找这两个验证函数auth1
和auth2
对于auth1
,secret
不是直接在constructor
中有定义了嘛~直接看合约
NICE!一下子就找到了根本难不倒我们~
但是当你开开心心的把123456输进去的时候,结果发现居然没通过???
咋回事嘞
再找找看咯
于是你在源码中发现了这个set_secret
!没想到owner
还可以改secret!!
这个我们玩区块链的根本不慌滴,区块链的每一笔交易都是有记录的,我们直接去看最早的交易记录.
嘿嘿!
这不就有了嘛~
看看这笔交易的信息
嘿嘿secret
就是0x154be90,转一下十进制就是22331024,还挺有寓意的嘛~
接下来就是搞auth2
的时候了
当我看到uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))
这个的时候,我瞬间乐开了花,这我可太熟悉不过了~
看看这篇文章Source of Randomness简直简直就是一个模子里刻出来的哇,
uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))
这玩意儿看着随机,其实是确定的!
接下来写个攻击合约就可以赚到大把大把的_balances
咯
直接上代码!
这里简单说明一下为什么将scoupon.coupon.loankey
赋值为2233通过testborrowtwice
后,在flashloan
函数中scoupon.coupon.loankey
又变为0,这是由于solidity0.8.12编译器自身原因导致了这个bug,从而使得第一个成员变量的值清零,在solidity0.8.16后这个问题得到了修复.
贴一下bug描述
至此,这第四题区块链的解答就到此结束了
说一说感受吧,每次做区块链的题目都感觉特别有意思,其实本人过去是学习逆向工程的 ,今年才开始接触区块链,解区块链题目的过程说实话,和逆向分析真的好像哇,都是一个逆向的过程,分析需要满足的条件,然后设法编写合约来让条件得到满足,最终满足所有需要的条件之后获得flag ,好玩好玩,嘿嘿(●ˇ∀ˇ●)
/
/
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_);
}
}
/
/
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;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2022-12-15 15:44
被oacia编辑
,原因: